matriz-analise-links / prompts.txt
alexandremoraisdarosa's picture
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>
&nbsp;&nbsp;- Verde: Link confirmado<br>
&nbsp;&nbsp;- Amarelo: Link suspeito<br>
&nbsp;&nbsp;- Vermelho: Link negativo (sem relação)<br>
&nbsp;&nbsp;- 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
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?