Spaces:
Running
Running
| document.addEventListener('DOMContentLoaded', () => { | |
| const matrixTable = document.getElementById('matrix-table'); | |
| const addEntityBtn = document.getElementById('addEntityBtn'); | |
| const newEntityInput = document.getElementById('newEntityInput'); | |
| const importInput = document.getElementById('importInput'); | |
| const noteModal = document.getElementById('note-modal'); | |
| const noteTextarea = document.getElementById('note-textarea'); | |
| const noteContext = document.getElementById('note-context'); | |
| let currentNoteCell = null; | |
| const RELATION_STATES = [ | |
| { class: 'bg-green-100 text-green-800 hover:bg-green-200', symbol: '✔', tooltip: 'Vínculo Confirmado' }, | |
| { class: 'bg-yellow-100 text-yellow-800 hover:bg-yellow-200', symbol: '∼', tooltip: 'Vínculo Possível' }, | |
| { class: 'bg-red-100 text-red-800 hover:bg-red-200', symbol: '✖', tooltip: 'Vínculo Excluído' }, | |
| { class: 'bg-gray-100 text-gray-600 hover:bg-gray-200', symbol: '•', tooltip: 'Não Analisado' } | |
| ]; | |
| let data = { entities: [], relations: {} }; | |
| function saveData() { | |
| localStorage.setItem('triangularMatrixData', JSON.stringify(data)); | |
| } | |
| function loadData() { | |
| const savedData = localStorage.getItem('triangularMatrixData'); | |
| if (savedData) { | |
| try { data = JSON.parse(savedData); } | |
| catch (e) { console.error("Erro ao carregar dados."); } | |
| } else { | |
| data = { | |
| entities: ["Réu João", "Testemunha Maria", "Laudo A", "Réu Pedro"], | |
| relations: { '1-0': { state: 2 }, '2-0': { state: 0, note: 'Laudo confirma digital do Réu João.' }, '3-1': { state: 1 } } | |
| }; | |
| } | |
| } | |
| function renderMatrix() { | |
| matrixTable.innerHTML = ''; | |
| const { entities } = data; | |
| if (entities.length < 2) { | |
| matrixTable.innerHTML = '<tr><td class="p-4 text-center text-gray-500">Adicione pelo menos duas entidades para iniciar a matriz.</td></tr>'; | |
| return; | |
| } | |
| // 1. Criar as linhas do corpo da matriz (cabeçalhos de linha + células) | |
| for (let rowIndex = 1; rowIndex < entities.length; rowIndex++) { | |
| const tr = document.createElement('tr'); | |
| // Cabeçalho da Linha (Eixo Y, à esquerda) | |
| const rowTh = document.createElement('th'); | |
| rowTh.className = 'min-w-[200px] bg-gray-50 border-b border-gray-300 p-2 text-right pr-4 whitespace-nowrap relative'; | |
| rowTh.innerHTML = `<span class="mr-1">${entities[rowIndex]}</span><span class="btn-delete text-red-500 hover:text-red-700 cursor-pointer text-sm" data-index="${rowIndex}">🗑️</span>`; | |
| tr.appendChild(rowTh); | |
| // Células de Relação | |
| for (let colIndex = 0; colIndex < rowIndex; colIndex++) { | |
| const td = document.createElement('td'); | |
| td.dataset.row = rowIndex; | |
| td.dataset.col = colIndex; | |
| const key = `${rowIndex}-${colIndex}`; | |
| const relation = data.relations[key] || { state: 3, note: '' }; | |
| const state = RELATION_STATES[relation.state]; | |
| td.className = `relation-cell border-b border-gray-300 text-center cursor-pointer relative text-3xl w-16 h-16 ${state.class}`; | |
| td.setAttribute('title', state.tooltip); | |
| td.innerHTML = `${state.symbol}<span class="note-indicator"></span>`; | |
| if (relation.note) { | |
| td.classList.add('has-note'); | |
| } | |
| tr.appendChild(td); | |
| } | |
| matrixTable.appendChild(tr); | |
| } | |
| // 2. Criar a última linha com os cabeçalhos das colunas (Eixo X, no fundo) | |
| const footerRow = document.createElement('tr'); | |
| // Canto inferior esquerdo (vazio) | |
| footerRow.appendChild(document.createElement('th')); | |
| // Cabeçalhos da Coluna | |
| for (let i = 0; i < entities.length - 1; i++) { | |
| const th = document.createElement('th'); | |
| th.className = 'bg-gray-50 border-gray-300 p-2 text-center align-top relative'; | |
| th.innerHTML = `<span class="mr-1">${entities[i]}</span><span class="btn-delete text-red-500 hover:text-red-700 cursor-pointer text-sm" data-index="${i}">🗑️</span>`; | |
| footerRow.appendChild(th); | |
| } | |
| matrixTable.appendChild(footerRow); | |
| saveData(); | |
| } | |
| function addEntity() { | |
| const name = newEntityInput.value.trim(); | |
| if (!name) { | |
| showToast('Por favor, digite o nome da entidade.', 'error'); | |
| newEntityInput.focus(); | |
| return; | |
| } | |
| if (data.entities.includes(name)) { | |
| showToast('Essa entidade já está na lista.', 'warning'); | |
| return; | |
| } | |
| data.entities.push(name); | |
| newEntityInput.value = ''; | |
| renderMatrix(); | |
| showToast('Entidade adicionada com sucesso!', 'success'); | |
| } | |
| function showToast(message, type = 'info') { | |
| const toast = document.createElement('div'); | |
| toast.className = `fixed bottom-4 right-4 px-4 py-2 rounded-md shadow-lg text-white ${ | |
| type === 'error' ? 'bg-red-500' : | |
| type === 'success' ? 'bg-green-500' : | |
| type === 'warning' ? 'bg-yellow-500' : 'bg-blue-500' | |
| }`; | |
| toast.textContent = message; | |
| document.body.appendChild(toast); | |
| setTimeout(() => { | |
| toast.classList.add('opacity-0', 'transition-opacity', 'duration-300'); | |
| setTimeout(() => toast.remove(), 300); | |
| }, 3000); | |
| } | |
| function deleteEntity(index) { | |
| const entityName = data.entities[index]; | |
| const modal = document.createElement('div'); | |
| modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50'; | |
| modal.innerHTML = ` | |
| <div class="bg-white rounded-lg shadow-lg max-w-md w-full p-6"> | |
| <h2 class="text-xl font-bold mb-4">Confirmar Exclusão</h2> | |
| <p class="mb-6">Tem certeza que deseja remover <strong>"${entityName}"</strong>? Todos os seus vínculos serão perdidos.</p> | |
| <div class="flex justify-end gap-2"> | |
| <button class="cancel-btn px-4 py-2 bg-gray-300 hover:bg-gray-400 rounded-md transition">Cancelar</button> | |
| <button class="confirm-btn px-4 py-2 bg-red-500 hover:bg-red-600 text-white rounded-md transition">Excluir</button> | |
| </div> | |
| </div> | |
| `; | |
| document.body.appendChild(modal); | |
| modal.querySelector('.cancel-btn').addEventListener('click', () => { | |
| modal.remove(); | |
| }); | |
| modal.querySelector('.confirm-btn').addEventListener('click', () => { | |
| data.entities.splice(index, 1); | |
| const newRelations = {}; | |
| for (const key in data.relations) { | |
| const [r, c] = key.split('-').map(Number); | |
| if (r === index || c === index) continue; | |
| let newR = r > index ? r - 1 : r; | |
| let newC = c > index ? c - 1 : c; | |
| newRelations[`${newR}-${newC}`] = data.relations[key]; | |
| } | |
| data.relations = newRelations; | |
| renderMatrix(); | |
| modal.remove(); | |
| showToast('Entidade removida com sucesso!', 'success'); | |
| }); | |
| } | |
| function getRelationKey(cell) { return `${cell.dataset.row}-${cell.dataset.col}`; } | |
| window.openNoteModal = (cell) => { | |
| currentNoteCell = cell; | |
| const key = getRelationKey(cell); | |
| const [row, col] = key.split('-').map(Number); | |
| noteContext.innerHTML = `Anotação entre: <strong>"${data.entities[row]}"</strong> e <strong>"${data.entities[col]}"</strong>`; | |
| noteTextarea.value = data.relations[key]?.note || ''; | |
| noteModal.classList.remove('hidden'); | |
| noteTextarea.focus(); | |
| }; | |
| window.closeNoteModal = () => { | |
| noteModal.classList.add('hidden'); | |
| currentNoteCell = null; | |
| }; | |
| window.saveNote = () => { | |
| if (!currentNoteCell) return; | |
| const key = getRelationKey(currentNoteCell); | |
| const note = noteTextarea.value.trim(); | |
| if (!data.relations[key]) data.relations[key] = { state: 3, note: '' }; | |
| data.relations[key].note = note; | |
| if (note) currentNoteCell.classList.add('has-note'); | |
| else currentNoteCell.classList.remove('has-note'); | |
| saveData(); | |
| closeNoteModal(); | |
| }; | |
| window.exportMatrix = () => { | |
| const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); | |
| const a = document.createElement('a'); | |
| a.href = URL.createObjectURL(blob); | |
| a.download = `matriz-vinculos-${new Date().toISOString().slice(0,10)}.json`; | |
| a.click(); | |
| URL.revokeObjectURL(a.href); | |
| }; | |
| window.clearAllData = () => { | |
| const modal = document.createElement('div'); | |
| modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50'; | |
| modal.innerHTML = ` | |
| <div class="bg-white rounded-lg shadow-lg max-w-md w-full p-6"> | |
| <h2 class="text-xl font-bold mb-4">Confirmar Limpeza</h2> | |
| <p class="mb-6">Tem certeza que deseja limpar TODOS os dados? Esta ação não pode ser desfeita.</p> | |
| <div class="flex justify-end gap-2"> | |
| <button class="cancel-btn px-4 py-2 bg-gray-300 hover:bg-gray-400 rounded-md transition">Cancelar</button> | |
| <button class="confirm-btn px-4 py-2 bg-red-500 hover:bg-red-600 text-white rounded-md transition">Limpar Tudo</button> | |
| </div> | |
| </div> | |
| `; | |
| document.body.appendChild(modal); | |
| modal.querySelector('.cancel-btn').addEventListener('click', () => { | |
| modal.remove(); | |
| }); | |
| modal.querySelector('.confirm-btn').addEventListener('click', () => { | |
| data = { entities: [], relations: {} }; | |
| localStorage.removeItem('triangularMatrixData'); | |
| renderMatrix(); | |
| modal.remove(); | |
| showToast('Todos os dados foram limpos!', 'success'); | |
| }); | |
| }; | |
| window.importMatrix = () => { importInput.click(); }; | |
| importInput.addEventListener('change', (e) => { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = () => { | |
| try { | |
| const importedData = JSON.parse(reader.result); | |
| if (importedData.entities && importedData.relations) { | |
| data = importedData; | |
| renderMatrix(); | |
| alert('✅ Matriz importada com sucesso!'); | |
| } else { | |
| alert('❌ Erro ao importar: O arquivo JSON não tem a estrutura esperada.'); | |
| } | |
| } catch (err) { alert('❌ Erro ao importar: formato de arquivo inválido.'); } | |
| }; | |
| reader.readAsText(file); | |
| e.target.value = ''; | |
| }); | |
| addEntityBtn.addEventListener('click', addEntity); | |
| newEntityInput.addEventListener('keypress', e => { if (e.key === 'Enter') addEntity(); }); | |
| matrixTable.addEventListener('click', e => { | |
| const cell = e.target.closest('.relation-cell'); | |
| if (cell) { | |
| cell.classList.add('scale-90'); | |
| setTimeout(() => cell.classList.remove('scale-90'), 200); | |
| const key = getRelationKey(cell); | |
| if (!data.relations[key]) data.relations[key] = { state: 3, note: '' }; | |
| data.relations[key].state = (data.relations[key].state + 1) % RELATION_STATES.length; | |
| // Feedback visual imediato antes do re-render | |
| const newState = RELATION_STATES[data.relations[key].state]; | |
| cell.className = `relation-cell border-b border-gray-300 text-center cursor-pointer relative text-3xl w-16 h-16 ${newState.class} scale-90`; | |
| cell.innerHTML = `${newState.symbol}<span class="note-indicator"></span>`; | |
| setTimeout(() => renderMatrix(), 200); // Re-render após animação | |
| return; | |
| } | |
| const deleteBtn = e.target.closest('.btn-delete'); | |
| if (deleteBtn) { | |
| deleteEntity(parseInt(deleteBtn.dataset.index)); | |
| } | |
| }); | |
| matrixTable.addEventListener('contextmenu', e => { | |
| const cell = e.target.closest('.relation-cell'); | |
| if (cell) { | |
| e.preventDefault(); | |
| openNoteModal(cell); | |
| } | |
| }); | |
| loadData(); | |
| renderMatrix(); | |
| }); |