Você poderia melhorar a visualização da Matriz de Links, colocando a coluna de cima em diagonal para facilitar a visualização e preenchimento? - Follow Up Deployment
6dc62e9
verified
Em português. Cores branca, preta e cinza; Criado e implementado com as dependÊncias. Armazenamento local. <!DOCTYPE html> | |
<html lang="pt-BR"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Sistema de Matriz de Análise de Links - Investigação Penal</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<style> | |
.matrix-cell { | |
width: 40px; | |
height: 40px; | |
border: 1px solid #d1d5db; | |
cursor: pointer; | |
transition: all 0.2s; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
font-size: 12px; | |
font-weight: bold; | |
} | |
.matrix-cell:hover { | |
background-color: #f3f4f6; | |
transform: scale(1.1); | |
} | |
.link-confirmed { | |
background-color: #22c55e !important; | |
color: white; | |
} | |
.link-suspected { | |
background-color: #f59e0b !important; | |
color: white; | |
} | |
.link-negative { | |
background-color: #ef4444 !important; | |
color: white; | |
} | |
.link-none { | |
background-color: #f9fafb; | |
color: #6b7280; | |
} | |
.entity-header { | |
writing-mode: vertical-rl; | |
text-orientation: mixed; | |
white-space: nowrap; | |
font-size: 11px; | |
padding: 4px; | |
background-color: #f3f4f6; | |
border: 1px solid #d1d5db; | |
min-height: 120px; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
} | |
.entity-row-header { | |
min-width: 120px; | |
padding: 4px; | |
background-color: #f3f4f6; | |
border: 1px solid #d1d5db; | |
font-size: 11px; | |
display: flex; | |
align-items: center; | |
text-align: left; | |
} | |
.matrix-diagonal { | |
background-color: #e5e7eb !important; | |
cursor: not-allowed; | |
} | |
@media print { | |
.no-print { display: none !important; } | |
} | |
.fade-in { | |
animation: fadeIn 0.3s ease-in; | |
} | |
@keyframes fadeIn { | |
from { opacity: 0; transform: translateY(-10px); } | |
to { opacity: 1; transform: translateY(0); } | |
} | |
</style> | |
</head> | |
<body class="bg-gray-50 min-h-screen"> | |
<!-- Header --> | |
<header class="bg-blue-900 text-white p-4 shadow-lg no-print"> | |
<div class="container mx-auto flex justify-between items-center"> | |
<h1 class="text-2xl font-bold"> | |
<i class="fas fa-project-diagram mr-2"></i> | |
Sistema de Matriz de Análise de Links | |
</h1> | |
<div class="flex space-x-2"> | |
<button onclick="clearAllData()" class="bg-red-600 hover:bg-red-700 px-4 py-2 rounded"> | |
<i class="fas fa-trash mr-1"></i> Limpar Dados | |
</button> | |
<button onclick="exportToExcel()" class="bg-green-600 hover:bg-green-700 px-4 py-2 rounded"> | |
<i class="fas fa-file-excel mr-1"></i> Excel | |
</button> | |
<button onclick="exportToImage()" class="bg-purple-600 hover:bg-purple-700 px-4 py-2 rounded"> | |
<i class="fas fa-image mr-1"></i> Imagem | |
</button> | |
<button onclick="showHelp()" class="bg-gray-600 hover:bg-gray-700 px-4 py-2 rounded"> | |
<i class="fas fa-question mr-1"></i> Ajuda | |
</button> | |
</div> | |
</div> | |
</header> | |
<div class="container mx-auto p-4"> | |
<!-- Tabs --> | |
<div class="mb-6 no-print"> | |
<div class="border-b border-gray-200"> | |
<nav class="-mb-px flex space-x-8"> | |
<button onclick="showTab('entities')" id="tab-entities" class="tab-button py-2 px-1 border-b-2 border-blue-500 text-blue-600 font-medium"> | |
<i class="fas fa-users mr-1"></i> Inventário de Entidades | |
</button> | |
<button onclick="showTab('matrix')" id="tab-matrix" class="tab-button py-2 px-1 border-b-2 border-transparent text-gray-500 hover:text-gray-700"> | |
<i class="fas fa-table mr-1"></i> Matriz de Links | |
</button> | |
<button onclick="showTab('analysis')" id="tab-analysis" class="tab-button py-2 px-1 border-b-2 border-transparent text-gray-500 hover:text-gray-700"> | |
<i class="fas fa-chart-line mr-1"></i> Análise | |
</button> | |
</nav> | |
</div> | |
</div> | |
<!-- Entities Tab --> | |
<div id="entities-tab" class="tab-content"> | |
<div class="bg-white rounded-lg shadow-md p-6 mb-6 fade-in"> | |
<h2 class="text-xl font-bold mb-4"> | |
<i class="fas fa-plus-circle mr-2"></i> Cadastro de Entidades | |
</h2> | |
<form id="entity-form" class="grid grid-cols-1 md:grid-cols-4 gap-4"> | |
<div> | |
<label class="block text-sm font-medium text-gray-700 mb-1">Nome *</label> | |
<input type="text" id="entity-name" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500" required> | |
</div> | |
<div> | |
<label class="block text-sm font-medium text-gray-700 mb-1">Tipo *</label> | |
<select id="entity-type" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"> | |
<option value="Pessoa">Pessoa</option> | |
<option value="Organização">Organização</option> | |
<option value="Local">Local</option> | |
<option value="Objeto">Objeto</option> | |
<option value="Evento">Evento</option> | |
<option value="Documento">Documento</option> | |
<option value="Veículo">Veículo</option> | |
<option value="Conta Bancária">Conta Bancária</option> | |
<option value="Telefone">Telefone</option> | |
<option value="Email">Email</option> | |
<option value="Endereço IP">Endereço IP</option> | |
<option value="Empresa">Empresa</option> | |
</select> | |
</div> | |
<div> | |
<label class="block text-sm font-medium text-gray-700 mb-1">Descrição</label> | |
<input type="text" id="entity-description" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="Detalhes adicionais..."> | |
</div> | |
<div class="flex items-end"> | |
<button type="submit" class="w-full bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition-colors"> | |
<i class="fas fa-save mr-1"></i> <span id="save-btn-text">Salvar</span> | |
</button> | |
</div> | |
</form> | |
</div> | |
<!-- Entities List --> | |
<div class="bg-white rounded-lg shadow-md p-6 fade-in"> | |
<div class="flex justify-between items-center mb-4"> | |
<h2 class="text-xl font-bold"> | |
<i class="fas fa-list mr-2"></i> Lista de Entidades (<span id="entity-count">0</span>) | |
</h2> | |
<div class="flex space-x-2"> | |
<input type="text" id="search-entities" placeholder="Buscar entidades..." class="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"> | |
<select id="filter-type" class="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"> | |
<option value="">Todos os tipos</option> | |
<option value="Pessoa">Pessoa</option> | |
<option value="Organização">Organização</option> | |
<option value="Local">Local</option> | |
<option value="Objeto">Objeto</option> | |
<option value="Evento">Evento</option> | |
<option value="Documento">Documento</option> | |
<option value="Veículo">Veículo</option> | |
<option value="Conta Bancária">Conta Bancária</option> | |
<option value="Telefone">Telefone</option> | |
<option value="Email">Email</option> | |
<option value="Endereço IP">Endereço IP</option> | |
<option value="Empresa">Empresa</option> | |
</select> | |
</div> | |
</div> | |
<div class="overflow-x-auto"> | |
<table class="min-w-full table-auto"> | |
<thead class="bg-gray-50"> | |
<tr> | |
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th> | |
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Nome</th> | |
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Tipo</th> | |
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Descrição</th> | |
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ações</th> | |
</tr> | |
</thead> | |
<tbody id="entities-table-body" class="bg-white divide-y divide-gray-200"> | |
<!-- Entities will be populated here --> | |
</tbody> | |
</table> | |
</div> | |
</div> | |
</div> | |
<!-- Matrix Tab --> | |
<div id="matrix-tab" class="tab-content hidden"> | |
<div class="bg-white rounded-lg shadow-md p-6 fade-in"> | |
<div class="flex justify-between items-center mb-4"> | |
<h2 class="text-xl font-bold"> | |
<i class="fas fa-table mr-2"></i> Matriz de Análise de Links | |
</h2> | |
<div class="flex space-x-2"> | |
<div class="flex items-center space-x-4 text-sm"> | |
<div class="flex items-center"> | |
<div class="w-4 h-4 bg-green-500 rounded mr-2"></div> | |
<span>Confirmado</span> | |
</div> | |
<div class="flex items-center"> | |
<div class="w-4 h-4 bg-yellow-500 rounded mr-2"></div> | |
<span>Suspeito</span> | |
</div> | |
<div class="flex items-center"> | |
<div class="w-4 h-4 bg-red-500 rounded mr-2"></div> | |
<span>Negativo</span> | |
</div> | |
<div class="flex items-center"> | |
<div class="w-4 h-4 bg-gray-100 border rounded mr-2"></div> | |
<span>Sem Link</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div id="matrix-container" class="overflow-auto max-h-96"> | |
<!-- Matrix will be generated here --> | |
</div> | |
</div> | |
</div> | |
<!-- Analysis Tab --> | |
<div id="analysis-tab" class="tab-content hidden"> | |
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6"> | |
<div class="bg-white rounded-lg shadow-md p-6 fade-in"> | |
<h2 class="text-xl font-bold mb-4"> | |
<i class="fas fa-chart-bar mr-2"></i> Estatísticas Gerais | |
</h2> | |
<div id="general-stats" class="space-y-4"> | |
<!-- Stats will be populated here --> | |
</div> | |
</div> | |
<div class="bg-white rounded-lg shadow-md p-6 fade-in"> | |
<h2 class="text-xl font-bold mb-4"> | |
<i class="fas fa-network-wired mr-2"></i> Análise de Conectividade | |
</h2> | |
<div id="connectivity-analysis"> | |
<!-- Connectivity analysis will be populated here --> | |
</div> | |
</div> | |
</div> | |
<div class="bg-white rounded-lg shadow-md p-6 mt-6 fade-in"> | |
<h2 class="text-xl font-bold mb-4"> | |
<i class="fas fa-exclamation-triangle mr-2"></i> Entidades de Alto Risco | |
</h2> | |
<div id="high-risk-entities"> | |
<!-- High risk entities will be populated here --> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Help Modal --> | |
<div id="help-modal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden z-50"> | |
<div class="flex items-center justify-center min-h-screen p-4"> | |
<div class="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-96 overflow-y-auto"> | |
<div class="p-6"> | |
<div class="flex justify-between items-center mb-4"> | |
<h2 class="text-2xl font-bold">Manual de Instruções</h2> | |
<button onclick="closeHelp()" class="text-gray-400 hover:text-gray-600"> | |
<i class="fas fa-times text-xl"></i> | |
</button> | |
</div> | |
<div class="space-y-6"> | |
<div> | |
<h3 class="text-lg font-semibold mb-2">1. Inventário de Entidades</h3> | |
<p class="mb-4">• Acesse a aba "Inventário de Entidades"<br> | |
• Preencha nome (obrigatório), tipo e descrição da entidade<br> | |
• Clique em "Salvar" para adicionar à lista<br> | |
• Use os filtros de busca e tipo para localizar entidades<br> | |
• Use os botões "Editar" ou "Excluir" para gerenciar entidades existentes</p> | |
</div> | |
<div> | |
<h3 class="text-lg font-semibold mb-2">2. Matriz de Links</h3> | |
<p class="mb-4">• Acesse a aba "Matriz de Links" após cadastrar pelo menos 2 entidades<br> | |
• Clique nas células da matriz para definir o tipo de relacionamento:<br> | |
- Verde: Link confirmado<br> | |
- Amarelo: Link suspeito<br> | |
- Vermelho: Link negativo (sem relação)<br> | |
- Cinza: Sem informação<br> | |
• A matriz é simétrica - ao definir um link, o oposto é automaticamente preenchido</p> | |
</div> | |
<div> | |
<h3 class="text-lg font-semibold mb-2">3. Análise</h3> | |
<p class="mb-4">• Visualize estatísticas gerais sobre entidades e links<br> | |
• Analise a conectividade entre entidades<br> | |
• Identifique entidades de alto risco com base no número de conexões</p> | |
</div> | |
<div> | |
<h3 class="text-lg font-semibold mb-2">4. Exportação</h3> | |
<p class="mb-4">• Excel: Exporta todas as entidades e links em planilha<br> | |
• Imagem: Gera imagem PNG da matriz atual<br> | |
• Os dados são salvos automaticamente no navegador</p> | |
</div> | |
</div> | |
</div> | |
</div> <script> | |
// Global variables | |
let entities = []; | |
let links = {}; | |
let currentEditingId = null; | |
// Initialize application | |
document.addEventListener('DOMContentLoaded', function() { | |
loadData(); | |
setupEventListeners(); | |
renderEntitiesTable(); | |
updateEntityCount(); | |
showTab('entities'); | |
}); | |
// Setup event listeners | |
function setupEventListeners() { | |
// Entity form submission | |
document.getElementById('entity-form').addEventListener('submit', function(e) { | |
e.preventDefault(); | |
saveEntity(); | |
}); | |
// Search and filter functionality | |
document.getElementById('search-entities').addEventListener('input', filterEntities); | |
document.getElementById('filter-type').addEventListener('change', filterEntities); | |
} | |
// Save entity (create or update) | |
function saveEntity() { | |
const name = document.getElementById('entity-name').value.trim(); | |
const type = document.getElementById('entity-type').value; | |
const description = document.getElementById('entity-description').value.trim(); | |
if (!name) { | |
alert('Por favor, preencha o nome da entidade.'); | |
return; | |
} | |
// Check for duplicate names (except when editing) | |
const existingEntity = entities.find(e => e.name.toLowerCase() === name.toLowerCase() && e.id !== currentEditingId); | |
if (existingEntity) { | |
alert('Já existe uma entidade com este nome.'); | |
return; | |
} | |
if (currentEditingId) { | |
// Update existing entity | |
const entityIndex = entities.findIndex(e => e.id === currentEditingId); | |
if (entityIndex !== -1) { | |
entities[entityIndex] = { | |
...entities[entityIndex], | |
name: name, | |
type: type, | |
description: description, | |
updatedAt: new Date().toISOString() | |
}; | |
} | |
currentEditingId = null; | |
document.getElementById('save-btn-text').textContent = 'Salvar'; | |
} else { | |
// Create new entity | |
const newEntity = { | |
id: Date.now(), | |
name: name, | |
type: type, | |
description: description, | |
createdAt: new Date().toISOString(), | |
updatedAt: new Date().toISOString() | |
}; | |
entities.push(newEntity); | |
} | |
// Clear form | |
document.getElementById('entity-form').reset(); | |
// Update UI | |
renderEntitiesTable(); | |
updateEntityCount(); | |
generateMatrix(); | |
updateAnalysis(); | |
saveData(); | |
// Show success message | |
showNotification('Entidade salva com sucesso!', 'success'); | |
} | |
// Edit entity | |
function editEntity(id) { | |
const entity = entities.find(e => e.id === id); | |
if (!entity) return; | |
document.getElementById('entity-name').value = entity.name; | |
document.getElementById('entity-type').value = entity.type; | |
document.getElementById('entity-description').value = entity.description || ''; | |
currentEditingId = id; | |
document.getElementById('save-btn-text').textContent = 'Atualizar'; | |
// Scroll to form | |
document.getElementById('entity-form').scrollIntoView({ behavior: 'smooth' }); | |
} | |
// Delete entity | |
function deleteEntity(id) { | |
if (!confirm('Tem certeza que deseja excluir esta entidade? Todos os links relacionados também serão removidos.')) { | |
return; | |
} | |
// Remove entity | |
entities = entities.filter(e => e.id !== id); | |
// Remove related links | |
Object.keys(links).forEach(key => { | |
if (key.includes(`-${id}-`) || key.includes(`-${id}`)) { | |
delete links[key]; | |
} | |
}); | |
// Update UI | |
renderEntitiesTable(); | |
updateEntityCount(); | |
generateMatrix(); | |
updateAnalysis(); | |
saveData(); | |
showNotification('Entidade excluída com sucesso!', 'success'); | |
} | |
// Render entities table | |
function renderEntitiesTable() { | |
const tbody = document.getElementById('entities-table-body'); | |
const searchTerm = document.getElementById('search-entities').value.toLowerCase(); | |
const filterType = document.getElementById('filter-type').value; | |
let filteredEntities = entities.filter(entity => { | |
const matchesSearch = entity.name.toLowerCase().includes(searchTerm) || | |
entity.description.toLowerCase().includes(searchTerm); | |
const matchesType = !filterType || entity.type === filterType; | |
return matchesSearch && matchesType; | |
}); | |
tbody.innerHTML = ''; | |
if (filteredEntities.length === 0) { | |
tbody.innerHTML = ` | |
<tr> | |
<td colspan="5" class="px-4 py-8 text-center text-gray-500"> | |
<i class="fas fa-inbox text-4xl mb-2"></i><br> | |
Nenhuma entidade encontrada | |
</td> | |
</tr> | |
`; | |
return; | |
} | |
filteredEntities.forEach(entity => { | |
const row = document.createElement('tr'); | |
row.className = 'hover:bg-gray-50 transition-colors'; | |
row.innerHTML = ` | |
<td class="px-4 py-2 text-sm text-gray-900">${entity.id}</td> | |
<td class="px-4 py-2"> | |
<div class="text-sm font-medium text-gray-900">${escapeHtml(entity.name)}</div> | |
</td> | |
<td class="px-4 py-2"> | |
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"> | |
${getEntityIcon(entity.type)} ${entity.type} | |
</span> | |
</td> | |
<td class="px-4 py-2 text-sm text-gray-500 max-w-xs truncate"> | |
${escapeHtml(entity.description || '-')} | |
</td> | |
<td class="px-4 py-2 text-sm font-medium space-x-2"> | |
<button onclick="editEntity(${entity.id})" class="text-blue-600 hover:text-blue-900 transition-colors"> | |
<i class="fas fa-edit"></i> Editar | |
</button> | |
<button onclick="deleteEntity(${entity.id})" class="text-red-600 hover:text-red-900 transition-colors"> | |
<i class="fas fa-trash"></i> Excluir | |
</button> | |
</td> | |
`; | |
tbody.appendChild(row); | |
}); | |
} | |
// Filter entities | |
function filterEntities() { | |
renderEntitiesTable(); | |
} | |
// Update entity count | |
function updateEntityCount() { | |
document.getElementById('entity-count').textContent = entities.length; | |
} | |
// Generate matrix | |
function generateMatrix() { | |
const container = document.getElementById('matrix-container'); | |
if (entities.length < 2) { | |
container.innerHTML = ` | |
<div class="text-center py-12 text-gray-500"> | |
<i class="fas fa-table text-4xl mb-4"></i> | |
<p class="text-lg">Adicione pelo menos 2 entidades para gerar a matriz</p> | |
</div> | |
`; | |
return; | |
} | |
const table = document.createElement('table'); | |
table.className = 'border-collapse'; | |
// Header row | |
const headerRow = document.createElement('tr'); | |
headerRow.appendChild(document.createElement('th')); // Empty corner cell | |
entities.forEach(entity => { | |
const th = document.createElement('th'); | |
th.className = 'entity-header'; | |
th.textContent = entity.name; | |
th.title = `${entity.name} (${entity.type})`; | |
headerRow.appendChild(th); | |
}); | |
table.appendChild(headerRow); | |
// Data rows | |
entities.forEach((rowEntity, rowIndex) => { | |
const row = document.createElement('tr'); | |
// Row header | |
const rowHeader = document.createElement('th'); | |
rowHeader.className = 'entity-row-header'; | |
rowHeader.textContent = rowEntity.name; | |
rowHeader.title = `${rowEntity.name} (${rowEntity.type})`; | |
row.appendChild(rowHeader); | |
// Data cells | |
entities.forEach((colEntity, colIndex) => { | |
const cell = document.createElement('td'); | |
cell.className = 'matrix-cell'; | |
if (rowIndex === colIndex) { | |
// Diagonal cell (same entity) | |
cell.className += ' matrix-diagonal'; | |
cell.textContent = '—'; | |
} else { | |
const linkKey = getLinkKey(rowEntity.id, colEntity.id); | |
const linkType = links[linkKey] || 'none'; | |
cell.className += ` link-${linkType}`; | |
cell.textContent = getLinkSymbol(linkType); | |
cell.onclick = () => toggleLink(rowEntity.id, colEntity.id); | |
cell.title = `${rowEntity.name} ↔ ${colEntity.name}`; | |
} | |
row.appendChild(cell); | |
}); | |
table.appendChild(row); | |
}); | |
container.innerHTML = ''; | |
container.appendChild(table); | |
} | |
// Toggle link between entities | |
function toggleLink(entityId1, entityId2) { | |
const linkKey = getLinkKey(entityId1, entityId2); | |
const currentType = links[linkKey] || 'none'; | |
const types = ['none', 'confirmed', 'suspected', 'negative']; | |
const currentIndex = types.indexOf(currentType); | |
const nextIndex = (currentIndex + 1) % types.length; | |
const nextType = types[nextIndex]; | |
if (nextType === 'none') { | |
delete links[linkKey]; | |
} else { | |
links[linkKey] = nextType; | |
} | |
generateMatrix(); | |
updateAnalysis(); | |
saveData(); | |
} | |
// Get link key (consistent ordering) | |
function getLinkKey(id1, id2) { | |
return id1 < id2 ? `${id1}-${id2}` : `${id2}-${id1}`; | |
} | |
// Get link symbol | |
function getLinkSymbol(linkType) { | |
switch (linkType) { | |
case 'confirmed': return '✓'; | |
case 'suspected': return '?'; | |
case 'negative': return '✗'; | |
default: return ''; | |
} | |
} | |
// Get entity icon | |
function getEntityIcon(type) { | |
const icons = { | |
'Pessoa': '👤', | |
'Organização': '🏢', | |
'Local': '📍', | |
'Objeto': '📦', | |
'Evento': '📅', | |
'Documento': '📄', | |
'Veículo': '🚗', | |
'Conta Bancária': '💳', | |
'Telefone': '📞', | |
'Email': '📧', | |
'Endereço IP': '🌐', | |
'Empresa': '🏭' | |
}; | |
return icons[type] || '📋'; | |
} | |
// Tab functionality | |
function showTab(tabName) { | |
// Hide all tabs | |
document.querySelectorAll('.tab-content').forEach(tab => { | |
tab.classList.add('hidden'); | |
}); | |
// Remove active class from all tab buttons | |
document.querySelectorAll('.tab-button').forEach(btn => { | |
btn.classList.remove('border-blue-500', 'text-blue-600'); | |
btn.classList.add('border-transparent', 'text-gray-500'); | |
}); | |
// Show selected tab | |
document.getElementById(`${tabName}-tab`).classList.remove('hidden'); | |
// Add active class to selected tab button | |
const activeBtn = document.getElementById(`tab-${tabName}`); | |
activeBtn.classList.remove('border-transparent', 'text-gray-500'); | |
activeBtn.classList.add('border-blue-500', 'text-blue-600'); | |
// Generate content based on tab | |
if (tabName === 'matrix') { | |
generateMatrix(); | |
} else if (tabName === 'analysis') { | |
updateAnalysis(); | |
} | |
} | |
// Update analysis | |
function updateAnalysis() { | |
updateGeneralStats(); | |
updateConnectivityAnalysis(); | |
updateHighRiskEntities(); | |
} | |
// Update general statistics | |
function updateGeneralStats() { | |
const statsContainer = document.getElementById('general-stats'); | |
const totalEntities = entities.length; | |
const totalLinks = Object.keys(links).length; | |
const confirmedLinks = Object.values(links).filter(type => type === 'confirmed').length; | |
const suspectedLinks = Object.values(links).filter(type => type === 'suspected').length; | |
const negativeLinks = Object.values(links).filter(type => type === 'negative').length; | |
const entityTypes = {}; | |
entities.forEach(entity => { | |
entityTypes[entity.type] = (entityTypes[entity.type] || 0) + 1; | |
}); | |
statsContainer.innerHTML = ` | |
<div class="grid grid-cols-2 gap-4"> | |
<div class="bg-blue-50 p-4 rounded-lg"> | |
<div class="text-2xl font-bold text-blue-600">${totalEntities}</div> | |
<div class="text-sm text-blue-600">Total de Entidades</div> | |
</div> | |
<div class="bg-green-50 p-4 rounded-lg"> | |
<div class="text-2xl font-bold text-green-600">${confirmedLinks}</div> | |
<div class="text-sm text-green-600">Links Confirmados</div> | |
</div> | |
<div class="bg-yellow-50 p-4 rounded-lg"> | |
<div class="text-2xl font-bold text-yellow-600">${suspectedLinks}</div> | |
<div class="text-sm text-yellow-600">Links Suspeitos</div> | |
</div> | |
<div class="bg-red-50 p-4 rounded-lg"> | |
<div class="text-2xl font-bold text-red-600">${negativeLinks}</div> | |
<div class="text-sm text-red-600">Links Negativos</div> | |
</div> | |
</div> | |
<div class="mt-6"> | |
<h3 class="font-semibold mb-3">Distribuição por Tipo:</h3> | |
<div class="space-y-2"> | |
${Object.entries(entityTypes).map(([type, count]) => ` | |
<div class="flex justify-between items-center"> | |
<span class="text-sm">${getEntityIcon(type)} ${type}</span> | |
<span class="bg-gray-100 px-2 py-1 rounded text-sm font-medium">${count}</span> | |
</div> | |
`).join('')} | |
</div> | |
</div> | |
`; | |
} | |
// Update connectivity analysis | |
function updateConnectivityAnalysis() { | |
const connectivityContainer = document.getElementById('connectivity-analysis'); | |
// Calculate connectivity for each entity | |
const connectivity = {}; | |
entities.forEach(entity => { | |
connectivity[entity.id] = { | |
entity: entity, | |
confirmed: 0, | |
suspected: 0, | |
negative: 0, | |
total: 0 | |
}; | |
}); | |
Object.entries(links).forEach(([linkKey, linkType]) => { | |
const [id1, id2] = linkKey.split('-').map(Number); | |
if (connectivity[id1] && connectivity[id2]) { | |
connectivity[id1][linkType]++; | |
connectivity[id2][linkType]++; | |
connectivity[id1].total++; | |
connectivity[id2].total++; | |
} | |
}); | |
const sortedEntities = Object.values(connectivity) | |
.sort((a, b) => b.total - a.total) | |
.slice(0, 10); | |
connectivityContainer.innerHTML = ` | |
<div class="space-y-3"> | |
${sortedEntities.map(conn => ` | |
<div class="border rounded-lg p-3"> | |
<div class="font-medium">${escapeHtml(conn.entity.name)}</div> | |
<div class="text-sm text-gray-600 mb-2">${conn.entity.type}</div> | |
<div class="flex space-x-4 text-sm"> | |
<span class="text-green-600">✓ ${conn.confirmed}</span> | |
<span class="text-yellow-600">? ${conn.suspected}</span> | |
<span class="text-red-600">✗ ${conn.negative}</span> | |
<span class="text-gray-600">Total: ${conn.total}</span> | |
</div> | |
</div> | |
`).join('')} | |
</div> | |
`; | |
} | |
// Update high risk entities | |
function updateHighRiskEntities() { | |
const highRiskContainer = document.getElementById('high-risk-entities'); | |
// Calculate risk score (confirmed + suspected links) | |
const riskScores = {}; | |
entities.forEach(entity => { | |
riskScores[entity.id] = { | |
entity: entity, | |
score: 0 | |
}; | |
}); | |
Object.entries(links).forEach(([linkKey, linkType]) => { | |
if (linkType === 'confirmed' || linkType === 'suspected') { | |
const [id1, id2] = linkKey.split('-').map(Number); | |
const weight = linkType === 'confirmed' ? 2 : 1; | |
if (riskScores[id1]) riskScores[id1].score += weight; | |
if (riskScores[id2]) riskScores[id2].score += weight; | |
} | |
}); | |
const highRiskEntities = Object.values(riskScores) | |
.filter(item => item.score > 0) | |
.sort((a, b) => b.score - a.score) | |
.slice(0, 10); | |
if (highRiskEntities.length === 0) { | |
highRiskContainer.innerHTML = ` | |
<div class="text-center py-8 text-gray-500"> | |
<i class="fas fa-shield-alt text-4xl mb-2"></i> | |
<p>Nenhuma entidade de alto risco identificada</p> | |
</div> | |
`; | |
return; | |
} | |
highRiskContainer.innerHTML = `<script> | |
// Global variables | |
let entities = []; | |
let links = {}; | |
let currentEditingId = null; | |
// Initialize application | |
document.addEventListener('DOMContentLoaded', function() { | |
loadData(); | |
setupEventListeners(); | |
renderEntitiesTable(); | |
updateEntityCount(); | |
showTab('entities'); | |
}); | |
// Setup event listeners | |
function setupEventListeners() { | |
// Entity form submission | |
document.getElementById('entity-form').addEventListener('submit', function(e) { | |
e.preventDefault(); | |
saveEntity(); | |
}); | |
// Search and filter functionality | |
document.getElementById('search-entities').addEventListener('input', filterEntities); | |
document.getElementById('filter-type').addEventListener('change', filterEntities); | |
} | |
// Save entity (create or update) | |
function saveEntity() { | |
const name = document.getElementById('entity-name').value.trim(); | |
const type = document.getElementById('entity-type').value; | |
const description = document.getElementById('entity-description').value.trim(); | |
if (!name) { | |
alert('Por favor, preencha o nome da entidade.'); | |
return; | |
} | |
// Check for duplicate names (except when editing) | |
const existingEntity = entities.find(e => e.name.toLowerCase() === name.toLowerCase() && e.id !== currentEditingId); | |
if (existingEntity) { | |
alert('Já existe uma entidade com este nome.'); | |
return; | |
} | |
if (currentEditingId) { | |
// Update existing entity | |
const entityIndex = entities.findIndex(e => e.id === currentEditingId); | |
if (entityIndex !== -1) { | |
entities[entityIndex] = { | |
...entities[entityIndex], | |
name: name, | |
type: type, | |
description: description, | |
updatedAt: new Date().toISOString() | |
}; | |
} | |
currentEditingId = null; | |
document.getElementById('save-btn-text').textContent = 'Salvar'; | |
} else { | |
// Create new entity | |
const newEntity = { | |
id: Date.now(), | |
name: name, | |
type: type, | |
description: description, | |
createdAt: new Date().toISOString(), | |
updatedAt: new Date().toISOString() | |
}; | |
entities.push(newEntity); | |
} | |
// Clear form | |
document.getElementById('entity-form').reset(); | |
// Update UI | |
renderEntitiesTable(); | |
updateEntityCount(); | |
generateMatrix(); | |
updateAnalysis(); | |
saveData(); | |
// Show success message | |
showNotification('Entidade salva com sucesso!', 'success'); | |
} | |
// Edit entity | |
function editEntity(id) { | |
const entity = entities.find(e => e.id === id); | |
if (!entity) return; | |
document.getElementById('entity-name').value = entity.name; | |
document.getElementById('entity-type').value = entity.type; | |
document.getElementById('entity-description').value = entity.description || ''; | |
currentEditingId = id; | |
document.getElementById('save-btn-text').textContent = 'Atualizar'; | |
// Scroll to form | |
document.getElementById('entity-form').scrollIntoView({ behavior: 'smooth' }); | |
} | |
// Delete entity | |
function deleteEntity(id) { | |
if (!confirm('Tem certeza que deseja excluir esta entidade? Todos os links relacionados também serão removidos.')) { | |
return; | |
} | |
// Remove entity | |
entities = entities.filter(e => e.id !== id); | |
// Remove related links | |
Object.keys(links).forEach(key => { | |
if (key.includes(`-${id}-`) || key.includes(`-${id}`)) { | |
delete links[key]; | |
} | |
}); | |
// Update UI | |
renderEntitiesTable(); | |
updateEntityCount(); | |
generateMatrix(); | |
updateAnalysis(); | |
saveData(); | |
showNotification('Entidade excluída com sucesso!', 'success'); | |
} | |
// Render entities table | |
function renderEntitiesTable() { | |
const tbody = document.getElementById('entities-table-body'); | |
const searchTerm = document.getElementById('search-entities').value.toLowerCase(); | |
const filterType = document.getElementById('filter-type').value; | |
let filteredEntities = entities.filter(entity => { | |
const matchesSearch = entity.name.toLowerCase().includes(searchTerm) || | |
entity.description.toLowerCase().includes(searchTerm); | |
const matchesType = !filterType || entity.type === filterType; | |
return matchesSearch && matchesType; | |
}); | |
tbody.innerHTML = ''; | |
if (filteredEntities.length === 0) { | |
tbody.innerHTML = ` | |
<tr> | |
<td colspan="5" class="px-4 py-8 text-center text-gray-500"> | |
<i class="fas fa-inbox text-4xl mb-2"></i><br> | |
Nenhuma entidade encontrada | |
</td> | |
</tr> | |
`; | |
return; | |
} | |
filteredEntities.forEach(entity => { | |
const row = document.createElement('tr'); | |
row.className = 'hover:bg-gray-50 transition-colors'; | |
row.innerHTML = ` | |
<td class="px-4 py-2 text-sm text-gray-900">${entity.id}</td> | |
<td class="px-4 py-2"> | |
<div class="text-sm font-medium text-gray-900">${escapeHtml(entity.name)}</div> | |
</td> | |
<td class="px-4 py-2"> | |
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"> | |
${getEntityIcon(entity.type)} ${entity.type} | |
</span> | |
</td> | |
<td class="px-4 py-2 text-sm text-gray-500 max-w-xs truncate"> | |
${escapeHtml(entity.description || '-')} | |
</td> | |
<td class="px-4 py-2 text-sm font-medium space-x-2"> | |
<button onclick="editEntity(${entity.id})" class="text-blue-600 hover:text-blue-900 transition-colors"> | |
<i class="fas fa-edit"></i> Editar | |
</button> | |
<button onclick="deleteEntity(${entity.id})" class="text-red-600 hover:text-red-900 transition-colors"> | |
<i class="fas fa-trash"></i> Excluir | |
</button> | |
</td> | |
`; | |
tbody.appendChild(row); | |
}); | |
} | |
// Filter entities | |
function filterEntities() { | |
renderEntitiesTable(); | |
} | |
// Update entity count | |
function updateEntityCount() { | |
document.getElementById('entity-count').textContent = entities.length; | |
} | |
// Generate matrix | |
function generateMatrix() { | |
const container = document.getElementById('matrix-container'); | |
if (entities.length < 2) { | |
container.innerHTML = ` | |
<div class="text-center py-12 text-gray-500"> | |
<i class="fas fa-table text-4xl mb-4"></i> | |
<p class="text-lg">Adicione pelo menos 2 entidades para gerar a matriz</p> | |
</div> | |
`; | |
return; | |
} | |
const table = document.createElement('table'); | |
table.className = 'border-collapse'; | |
// Header row | |
const headerRow = document.createElement('tr'); | |
headerRow.appendChild(document.createElement('th')); // Empty corner cell | |
entities.forEach(entity => { | |
const th = document.createElement('th'); | |
th.className = 'entity-header'; | |
th.textContent = entity.name; | |
th.title = `${entity.name} (${entity.type})`; | |
headerRow.appendChild(th); | |
}); | |
table.appendChild(headerRow); | |
// Data rows | |
entities.forEach((rowEntity, rowIndex) => { | |
const row = document.createElement('tr'); | |
// Row header | |
const rowHeader = document.createElement('th'); | |
rowHeader.className = 'entity-row-header'; | |
rowHeader.textContent = rowEntity.name; | |
rowHeader.title = `${rowEntity.name} (${rowEntity.type})`; | |
row.appendChild(rowHeader); | |
// Data cells | |
entities.forEach((colEntity, colIndex) => { | |
const cell = document.createElement('td'); | |
cell.className = 'matrix-cell'; | |
if (rowIndex === colIndex) { | |
// Diagonal cell (same entity) | |
cell.className += ' matrix-diagonal'; | |
cell.textContent = '—'; | |
} else { | |
const linkKey = getLinkKey(rowEntity.id, colEntity.id); | |
const linkType = links[linkKey] || 'none'; | |
cell.className += ` link-${linkType}`; | |
cell.textContent = getLinkSymbol(linkType); | |
cell.onclick = () => toggleLink(rowEntity.id, colEntity.id); | |
cell.title = `${rowEntity.name} ↔ ${colEntity.name}`; | |
} | |
row.appendChild(cell); | |
}); | |
table.appendChild(row); | |
}); | |
container.innerHTML = ''; | |
container.appendChild(table); | |
} | |
// Toggle link between entities | |
function toggleLink(entityId1, entityId2) { | |
const linkKey = getLinkKey(entityId1, entityId2); | |
const currentType = links[linkKey] || 'none'; | |
const types = ['none', 'confirmed', 'suspected', 'negative']; | |
const currentIndex = types.indexOf(currentType); | |
const nextIndex = (currentIndex + 1) % types.length; | |
const nextType = types[nextIndex]; | |
if (nextType === 'none') { | |
delete links[linkKey]; | |
} else { | |
links[linkKey] = nextType; | |
} | |
generateMatrix(); | |
updateAnalysis(); | |
saveData(); | |
} | |
// Get link key (consistent ordering) | |
function getLinkKey(id1, id2) { | |
return id1 < id2 ? `${id1}-${id2}` : `${id2}-${id1}`; | |
} | |
// Get link symbol | |
function getLinkSymbol(linkType) { | |
switch (linkType) { | |
case 'confirmed': return '✓'; | |
case 'suspected': return '?'; | |
case 'negative': return '✗'; | |
default: return ''; | |
} | |
} | |
// Get entity icon | |
function getEntityIcon(type) { | |
const icons = { | |
'Pessoa': '👤', | |
'Organização': '🏢', | |
'Local': '📍', | |
'Objeto': '📦', | |
'Evento': '📅', | |
'Documento': '📄', | |
'Veículo': '🚗', | |
'Conta Bancária': '💳', | |
'Telefone': '📞', | |
'Email': '📧', | |
'Endereço IP': '🌐', | |
'Empresa': '🏭' | |
}; | |
return icons[type] || '📋'; | |
} | |
// Tab functionality | |
function showTab(tabName) { | |
// Hide all tabs | |
document.querySelectorAll('.tab-content').forEach(tab => { | |
tab.classList.add('hidden'); | |
}); | |
// Remove active class from all tab buttons | |
document.querySelectorAll('.tab-button').forEach(btn => { | |
btn.classList.remove('border-blue-500', 'text-blue-600'); | |
btn.classList.add('border-transparent', 'text-gray-500'); | |
}); | |
// Show selected tab | |
document.getElementById(`${tabName}-tab`).classList.remove('hidden'); | |
// Add active class to selected tab button | |
const activeBtn = document.getElementById(`tab-${tabName}`); | |
activeBtn.classList.remove('border-transparent', 'text-gray-500'); | |
activeBtn.classList.add('border-blue-500', 'text-blue-600'); | |
// Generate content based on tab | |
if (tabName === 'matrix') { | |
generateMatrix(); | |
} else if (tabName === 'analysis') { | |
updateAnalysis(); | |
} | |
} | |
// Update analysis | |
function updateAnalysis() { | |
updateGeneralStats(); | |
updateConnectivityAnalysis(); | |
updateHighRiskEntities(); | |
} | |
// Update general statistics | |
function updateGeneralStats() { | |
const statsContainer = document.getElementById('general-stats'); | |
const totalEntities = entities.length; | |
const totalLinks = Object.keys(links).length; | |
const confirmedLinks = Object.values(links).filter(type => type === 'confirmed').length; | |
const suspectedLinks = Object.values(links).filter(type => type === 'suspected').length; | |
const negativeLinks = Object.values(links).filter(type => type === 'negative').length; | |
const entityTypes = {}; | |
entities.forEach(entity => { | |
entityTypes[entity.type] = (entityTypes[entity.type] || 0) + 1; | |
}); | |
statsContainer.innerHTML = ` | |
<div class="grid grid-cols-2 gap-4"> | |
<div class="bg-blue-50 p-4 rounded-lg"> | |
<div class="text-2xl font-bold text-blue-600">${totalEntities}</div> | |
<div class="text-sm text-blue-600">Total de Entidades</div> | |
</div> | |
<div class="bg-green-50 p-4 rounded-lg"> | |
<div class="text-2xl font-bold text-green-600">${confirmedLinks}</div> | |
<div class="text-sm text-green-600">Links Confirmados</div> | |
</div> | |
<div class="bg-yellow-50 p-4 rounded-lg"> | |
<div class="text-2xl font-bold text-yellow-600">${suspectedLinks}</div> | |
<div class="text-sm text-yellow-600">Links Suspeitos</div> | |
</div> | |
<div class="bg-red-50 p-4 rounded-lg"> | |
<div class="text-2xl font-bold text-red-600">${negativeLinks}</div> | |
<div class="text-sm text-red-600">Links Negativos</div> | |
</div> | |
</div> | |
<div class="mt-6"> | |
<h3 class="font-semibold mb-3">Distribuição por Tipo:</h3> | |
<div class="space-y-2"> | |
${Object.entries(entityTypes).map(([type, count]) => ` | |
<div class="flex justify-between items-center"> | |
<span class="text-sm">${getEntityIcon(type)} ${type}</span> | |
<span class="bg-gray-100 px-2 py-1 rounded text-sm font-medium">${count}</span> | |
</div> | |
`).join('')} | |
</div> | |
</div> | |
`; | |
} | |
// Update connectivity analysis | |
function updateConnectivityAnalysis() { | |
const connectivityContainer = document.getElementById('connectivity-analysis'); | |
// Calculate connectivity for each entity | |
const connectivity = {}; | |
entities.forEach(entity => { | |
connectivity[entity.id] = { | |
entity: entity, | |
confirmed: 0, | |
suspected: 0, | |
negative: 0, | |
total: 0 | |
}; | |
}); | |
Object.entries(links).forEach(([linkKey, linkType]) => { | |
const [id1, id2] = linkKey.split('-').map(Number); | |
if (connectivity[id1] && connectivity[id2]) { | |
connectivity[id1][linkType]++; | |
connectivity[id2][linkType]++; | |
connectivity[id1].total++; | |
connectivity[id2].total++; | |
} | |
}); | |
const sortedEntities = Object.values(connectivity) | |
.sort((a, b) => b.total - a.total) | |
.slice(0, 10); | |
connectivityContainer.innerHTML = ` | |
<div class="space-y-3"> | |
${sortedEntities.map(conn => ` | |
<div class="border rounded-lg p-3"> | |
<div class="font-medium">${escapeHtml(conn.entity.name)}</div> | |
<div class="text-sm text-gray-600 mb-2">${conn.entity.type}</div> | |
<div class="flex space-x-4 text-sm"> | |
<span class="text-green-600">✓ ${conn.confirmed}</span> | |
<span class="text-yellow-600">? ${conn.suspected}</span> | |
<span class="text-red-600">✗ ${conn.negative}</span> | |
<span class="text-gray-600">Total: ${conn.total}</span> | |
</div> | |
</div> | |
`).join('')} | |
</div> | |
`; | |
} | |
// Update high risk entities | |
function updateHighRiskEntities() { | |
const highRiskContainer = document.getElementById('high-risk-entities'); | |
// Calculate risk score (confirmed + suspected links) | |
const riskScores = {}; | |
entities.forEach(entity => { | |
riskScores[entity.id] = { | |
entity: entity, | |
score: 0 | |
}; | |
}); | |
Object.entries(links).forEach(([linkKey, linkType]) => { | |
if (linkType === 'confirmed' || linkType === 'suspected') { | |
const [id1, id2] = linkKey.split('-').map(Number); | |
const weight = linkType === 'confirmed' ? 2 : 1; | |
if (riskScores[id1]) riskScores[id1].score += weight; | |
if (riskScores[id2]) riskScores[id2].score += weight; | |
} | |
}); | |
const highRiskEntities = Object.values(riskScores) | |
.filter(item => item.score > 0) | |
.sort((a, b) => b.score - a.score) | |
.slice(0, 10); | |
if (highRiskEntities.length === 0) { | |
highRiskContainer.innerHTML = ` | |
<div class="text-center py-8 text-gray-500"> | |
<i class="fas fa-shield-alt text-4xl mb-2"></i> | |
<p>Nenhuma entidade de alto risco identificada</p> | |
</div> | |
`; | |
return; | |
} | |
highRiskContainer.innerHTML = ` <div class="space-y-3"> | |
${highRiskEntities.map(item => ` | |
<div class="border rounded-lg p-3 bg-gradient-to-r from-red-50 to-orange-50"> | |
<div class="flex justify-between items-start"> | |
<div> | |
<div class="font-medium text-gray-900">${escapeHtml(item.entity.name)}</div> | |
<div class="text-sm text-gray-600 mb-1">${getEntityIcon(item.entity.type)} ${item.entity.type}</div> | |
<div class="text-xs text-gray-500">${escapeHtml(item.entity.description || 'Sem descrição')}</div> | |
</div> | |
<div class="text-right"> | |
<div class="text-lg font-bold text-red-600">${item.score}</div> | |
<div class="text-xs text-red-500">Score de Risco</div> | |
</div> | |
</div> | |
</div> | |
`).join('')} | |
</div> | |
`; | |
} else { | |
analysisContent.innerHTML = ` | |
<div class="text-center text-gray-500 py-8"> | |
<i class="fas fa-chart-line text-4xl mb-4"></i> | |
<p>Adicione pelo menos 2 entidades e alguns links para ver a análise</p> | |
</div> | |
`; | |
} | |
} | |
// Export functions | |
function exportToExcel() { | |
if (entities.length === 0) { | |
showNotification('Nenhuma entidade para exportar', 'error'); | |
return; | |
} | |
try { | |
const wb = XLSX.utils.book_new(); | |
const entitiesData = entities.map(entity => ({ | |
'ID': entity.id, | |
'Nome': entity.name, | |
'Tipo': entity.type, | |
'Descrição': entity.description || '', | |
'Criado em': new Date(entity.createdAt).toLocaleString('pt-BR'), | |
'Atualizado em': new Date(entity.updatedAt).toLocaleString('pt-BR') | |
})); | |
const entitiesWs = XLSX.utils.json_to_sheet(entitiesData); | |
XLSX.utils.book_append_sheet(wb, entitiesWs, 'Entidades'); | |
const linksData = []; | |
Object.entries(links).forEach(([linkKey, linkType]) => { | |
const [id1, id2] = linkKey.split('-').map(Number); | |
const entity1 = entities.find(e => e.id === id1); | |
const entity2 = entities.find(e => e.id === id2); | |
if (entity1 && entity2) { | |
linksData.push({ | |
'Entidade 1': entity1.name, | |
'Tipo 1': entity1.type, | |
'Entidade 2': entity2.name, | |
'Tipo 2': entity2.type, | |
'Tipo de Link': linkType, | |
'Símbolo': getLinkSymbol(linkType) | |
}); | |
} | |
}); | |
if (linksData.length > 0) { | |
const linksWs = XLSX.utils.json_to_sheet(linksData); | |
XLSX.utils.book_append_sheet(wb, linksWs, 'Links'); | |
} | |
if (entities.length >= 2) { | |
const matrixData = []; | |
const headerRow = ['']; | |
entities.forEach(entity => headerRow.push(entity.name)); | |
matrixData.push(headerRow); | |
entities.forEach(rowEntity => { | |
const row = [rowEntity.name]; | |
entities.forEach(colEntity => { | |
if (rowEntity.id === colEntity.id) { | |
row.push('—'); | |
} else { | |
const linkKey = getLinkKey(rowEntity.id, colEntity.id); | |
const linkType = links[linkKey] || 'none'; | |
row.push(getLinkSymbol(linkType) || ''); | |
} | |
}); | |
matrixData.push(row); | |
}); | |
const matrixWs = XLSX.utils.aoa_to_sheet(matrixData); | |
XLSX.utils.book_append_sheet(wb, matrixWs, 'Matriz'); | |
} | |
const fileName = `analise_links_${new Date().toISOString().split('T')[0]}.xlsx`; | |
XLSX.writeFile(wb, fileName); | |
showNotification('Dados exportados com sucesso!', 'success'); | |
} catch (error) { | |
console.error('Erro ao exportar:', error); | |
showNotification('Erro ao exportar dados', 'error'); | |
} | |
} | |
function exportToImage() { | |
const matrixContainer = document.getElementById('matrix-container'); | |
const table = matrixContainer.querySelector('table'); | |
if (!table) { | |
showNotification('Nenhuma matriz para exportar', 'error'); | |
return; | |
} | |
try { | |
html2canvas(table, { | |
backgroundColor: '#ffffff', | |
scale: 2, | |
logging: false, | |
useCORS: true | |
}).then(canvas => { | |
const link = document.createElement('a'); | |
link.download = `matriz_analise_${new Date().toISOString().split('T')[0]}.png`; | |
link.href = canvas.toDataURL(); | |
link.click(); | |
showNotification('Imagem exportada com sucesso!', 'success'); | |
}).catch(error => { | |
console.error('Erro ao gerar imagem:', error); | |
showNotification('Erro ao exportar imagem', 'error'); | |
}); | |
} catch (error) { | |
console.error('Erro ao exportar imagem:', error); | |
showNotification('Erro ao exportar imagem', 'error'); | |
} | |
} | |
function importData() { | |
const input = document.createElement('input'); | |
input.type = 'file'; | |
input.accept = '.json'; | |
input.onchange = function(e) { | |
const file = e.target.files[0]; | |
if (!file) return; | |
const reader = new FileReader(); | |
reader.onload = function(e) { | |
try { | |
const data = JSON.parse(e.target.result); | |
if (data.entities && Array.isArray(data.entities)) { | |
if (confirm('Isso substituirá todos os dados atuais. Continuar?')) { | |
entities = data.entities; | |
links = data.links || {}; | |
renderEntitiesTable(); | |
updateEntityCount(); | |
generateMatrix(); | |
updateAnalysis(); | |
saveData(); | |
showNotification('Dados importados com sucesso!', 'success'); | |
} | |
} else { | |
showNotification('Formato de arquivo inválido', 'error'); | |
} | |
} catch (error) { | |
console.error('Erro ao importar:', error); | |
showNotification('Erro ao importar dados', 'error'); | |
} | |
}; | |
reader.readAsText(file); | |
}; | |
input.click(); | |
} | |
function exportToJSON() { | |
if (entities.length === 0) { | |
showNotification('Nenhuma entidade para exportar', 'error'); | |
return; | |
} | |
try { | |
const data = { | |
entities: entities, | |
links: links, | |
exportDate: new Date().toISOString(), | |
version: '1.0' | |
}; | |
const dataStr = JSON.stringify(data, null, 2); | |
const dataBlob = new Blob([dataStr], { type: 'application/json' }); | |
const link = document.createElement('a'); | |
link.href = URL.createObjectURL(dataBlob); | |
link.download = `analise_links_${new Date().toISOString().split('T')[0]}.json`; | |
link.click(); | |
showNotification('Dados exportados em JSON com sucesso!', 'success'); | |
} catch (error) { | |
console.error('Erro ao exportar JSON:', error); | |
showNotification('Erro ao exportar dados', 'error'); | |
} | |
} | |
function clearAllData() { | |
if (!confirm('ATENÇÃO: Isso apagará TODOS os dados permanentemente. Esta ação não pode ser desfeita. Continuar?')) { | |
return; | |
} | |
if (!confirm('Tem ABSOLUTA certeza? Todos os dados serão perdidos!')) { | |
return; | |
} | |
entities = []; | |
links = {}; | |
currentEditingId = null; | |
document.getElementById('entity-form').reset(); | |
document.getElementById('save-btn-text').textContent = 'Salvar'; | |
renderEntitiesTable(); | |
updateEntityCount(); | |
generateMatrix(); | |
updateAnalysis(); | |
saveData(); | |
showNotification('Todos os dados foram apagados', 'success'); | |
} | |
function printMatrix() { | |
const matrixContainer = document.getElementById('matrix-container'); | |
const table = matrixContainer.querySelector('table'); | |
if (!table) { | |
showNotification('Nenhuma matriz para imprimir', 'error'); | |
return; | |
} | |
const printWindow = window.open('', '_blank'); | |
printWindow.document.write(` | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Matriz de Análise de Links</title> | |
<style> | |
body { font-family: Arial, sans-serif; margin: 20px; } | |
h1 { color: #1f2937; margin-bottom: 20px; } | |
table { border-collapse: collapse; width: 100%; } | |
th, td { border: 1px solid #d1d5db; padding: 8px; text-align: center; } | |
th { background-color: #f3f4f6; font-weight: bold; } | |
.matrix-diagonal { background-color: #f9fafb; } | |
.link-confirmed { background-color: #dcfce7; color: #16a34a; } | |
.link-suspected { background-color: #fef3c7; color: #d97706; } | |
.link-negative { background-color: #fee2e2; color: #dc2626; } | |
.entity-header, .entity-row-header { | |
background-color: #e5e7eb; | |
font-weight: bold; | |
writing-mode: vertical-rl; | |
text-orientation: mixed; | |
max-width: 30px; | |
word-wrap: break-word; | |
} | |
.print-info { margin-bottom: 20px; color: #6b7280; } | |
@media print { | |
body { margin: 0; } | |
.no-print { display: none; } | |
} | |
</style> | |
</head> | |
<body> | |
<h1>Matriz de Análise de Links</h1> | |
<div class="print-info"> | |
Gerado em: ${new Date().toLocaleString('pt-BR')}<br> | |
Total de entidades: ${entities.length}<br> | |
Total de links: ${Object.keys(links).length} | |
</div> | |
${table.outerHTML} | |
<div style="margin-top: 20px; font-size: 12px; color: #6b7280;"> | |
<strong>Legenda:</strong><br> | |
✓ = Link Confirmado | ? = Link Suspeito | ✗ = Link Negativo | — = Mesma Entidade | |
</div> | |
</body> | |
</html> | |
`); | |
printWindow.document.close(); | |
printWindow.focus(); | |
setTimeout(() => { | |
printWindow.print(); | |
printWindow.close(); | |
}, 250); | |
} | |
function saveData() { | |
try { | |
localStorage.setItem('criminal_analysis_entities', JSON.stringify(entities)); | |
localStorage.setItem('criminal_analysis_links', JSON.stringify(links)); | |
localStorage.setItem('criminal_analysis_last_save', new Date().toISOString()); | |
} catch (error) { | |
console.error('Erro ao salvar dados:', error); | |
showNotification('Erro ao salvar dados localmente', 'error'); | |
} | |
} | |
function loadData() { | |
try { | |
const savedEntities = localStorage.getItem('criminal_analysis_entities'); | |
const savedLinks = localStorage.getItem('criminal_analysis_links'); | |
entities = savedEntities ? JSON.parse(savedEntities) : []; | |
links = savedLinks ? JSON.parse(savedLinks) : {}; | |
entities = entities.filter(entity => | |
entity && entity.id && entity.name && entity.type | |
); | |
const validEntityIds = new Set(entities.map(e => e.id)); | |
Object.keys(links).forEach(linkKey => { | |
const [id1, id2] = linkKey.split('-').map(Number); | |
if (!validEntityIds.has(id1) || !validEntityIds.has(id2)) { | |
delete links[linkKey]; | |
} | |
}); | |
} catch (error) { | |
console.error('Erro ao carregar dados:', error); | |
entities = []; | |
links = {}; | |
showNotification('Erro ao carregar dados salvos', 'error'); | |
} | |
} | |
function showNotification(message, type = 'info') { | |
const notification = document.createElement('div'); | |
const bgColor = { | |
success: 'bg-green-500', | |
error: 'bg-red-500', | |
warning: 'bg-yellow-500', | |
info: 'bg-blue-500' | |
}[type] || 'bg-blue-500'; | |
const icon = { | |
success: 'fas fa-check-circle', | |
error: 'fas fa-exclamation-circle', | |
warning: 'fas fa-exclamation-triangle', | |
info: 'fas fa-info-circle' | |
}[type] || 'fas fa-info-circle'; | |
notification.className = `fixed bottom-4 right-4 ${bgColor} text-white p-4 rounded-lg shadow-lg z-50 max-w-sm transform transition-all duration-300 translate-x-full`; | |
notification.innerHTML = ` | |
<div class="flex items-center space-x-2"> | |
<i class="${icon}"></i> | |
<span class="font-medium">${escapeHtml(message)}</span> | |
</div> | |
`; | |
document.body.appendChild(notification); | |
setTimeout(() => { | |
notification.classList.remove('translate-x-full'); | |
}, 100); | |
setTimeout(() => { | |
notification.classList.add('translate-x-full'); | |
setTimeout(() => { | |
if (notification.parentNode) { | |
notification.remove(); | |
} | |
}, 300); | |
}, 4000); | |
} | |
function escapeHtml(unsafe) { | |
if (typeof unsafe !== 'string') return ''; | |
return unsafe | |
.replace(/&/g, '&') | |
.replace(/</g, '<') | |
.replace(/>/g, '>') | |
.replace(/"/g, '"') | |
.replace(/'/g, '''); | |
} | |
let autoSaveInterval; | |
function startAutoSave() { | |
autoSaveInterval = setInterval(() => { | |
saveData(); | |
}, 30000); | |
} | |
function stopAutoSave() { | |
if (autoSaveInterval) { | |
clearInterval(autoSaveInterval); | |
} | |
} | |
document.addEventListener('keydown', function(e) { | |
if (e.ctrlKey && e.key === 's') { | |
e.preventDefault(); | |
saveData(); | |
showNotification('Dados salvos manualmente', 'success'); | |
} | |
if (e.ctrlKey && e.key === 'e') { | |
e.preventDefault(); | |
exportToExcel(); | |
} | |
if (e.key === 'Escape' && currentEditingId) { | |
currentEditingId = null; | |
document.getElementById('entity-form').reset(); | |
document.getElementById('save-btn-text').textContent = 'Salvar'; | |
showNotification('Edição cancelada', 'info'); | |
} | |
}); | |
document.addEventListener('DOMContentLoaded', function() { | |
startAutoSave(); | |
}); | |
window.addEventListener('beforeunload', function(e) { | |
saveData(); | |
stopAutoSave(); | |
}); | |
function performAdvancedSearch() { | |
const searchTerm = document.getElementById('search-entities').value.toLowerCase(); | |
const filterType = document.getElementById('filter-type').value; | |
if (!searchTerm && !filterType) { | |
renderEntitiesTable(); | |
return; | |
} | |
const results = entities.filter(entity => { | |
const matchesSearch = !searchTerm || | |
entity.name.toLowerCase().includes(searchTerm) || | |
entity.description.toLowerCase().includes(searchTerm) || | |
entity.type.toLowerCase().includes(searchTerm); | |
const matchesType = !filterType || entity.type === filterType; | |
return matchesSearch && matchesType; | |
}); | |
const tbody = document.getElementById('entities-table-body'); | |
tbody.innerHTML = ''; | |
if (results.length === 0) { | |
tbody.innerHTML = ` | |
<tr> | |
<td colspan="5" class="px-6 py-8 text-center text-gray-500"> | |
<i class="fas fa-search text-2xl mb-2"></i> | |
<p>Nenhum resultado encontrado para sua busca</p> | |
</td> | |
</tr> | |
`; | |
} else { | |
results.forEach(entity => { | |
const row = document.createElement('tr'); | |
row.className = 'hover:bg-gray-50'; | |
row.innerHTML = ` | |
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${entity.id}</td> | |
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${escapeHtml(entity.name)}</td> | |
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> | |
${getEntityIcon(entity.type)} ${entity.type} | |
</td> | |
<td class="px-6 py-4 text-sm text-gray-500 max-w | |
Você poderia melhorar a visualização da Matriz de Links, colocando a coluna de cima em diagonal para facilitar a visualização e preenchimento? |