Files
SteelBase/public/js/ui/csv-manager-ui.js

403 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* CSV Manager UI
* User interface for CRUD operations on CSV files
*/
import {
loadCSV,
toCSV,
downloadCSV,
getAvailableCSVFiles,
validateCSVData
} from '../utils/csv-manager.js';
// Current state
let currentCSVData = [];
let currentFilename = '';
let currentFileId = '';
let editingIndex = -1;
/**
* Open CSV Manager Modal
*/
export function openCSVManager() {
const modal = document.getElementById('csvManagerModal');
const select = document.getElementById('csvFileSelect');
// Populate file selector
const files = getAvailableCSVFiles();
select.innerHTML = '<option value="">-- Selecione um arquivo --</option>';
files.forEach(file => {
const option = document.createElement('option');
option.value = file.id;
option.textContent = `${file.icon} ${file.name} - ${file.description}`;
select.appendChild(option);
});
modal.classList.add('active');
}
/**
* Close CSV Manager Modal
*/
export function closeCSVManager() {
const modal = document.getElementById('csvManagerModal');
modal.classList.remove('active');
// Reset state
currentCSVData = [];
currentFilename = '';
currentFileId = '';
// Reset UI
document.getElementById('csvFileSelect').value = '';
document.getElementById('csvContent').innerHTML = `
<div style="text-align: center; padding: 60px 20px; color: var(--color-text-secondary);">
<div style="font-size: 64px; margin-bottom: 16px;">📁</div>
<p style="font-size: 18px; margin-bottom: 8px;">Selecione um arquivo CSV para começar</p>
<p style="font-size: 14px;">Você poderá visualizar, editar, adicionar e remover registros</p>
</div>
`;
// Hide buttons
document.getElementById('btnAddRecord').style.display = 'none';
document.getElementById('btnDownload').style.display = 'none';
}
/**
* Load selected CSV file
*/
export async function loadSelectedCSV() {
const select = document.getElementById('csvFileSelect');
const fileId = select.value;
if (!fileId) {
closeCSVManager();
openCSVManager();
return;
}
const files = getAvailableCSVFiles();
const file = files.find(f => f.id === fileId);
if (!file) return;
// Show loading
document.getElementById('csvContent').innerHTML = `
<div style="text-align: center; padding: 60px 20px;">
<div style="font-size: 48px; margin-bottom: 16px;">⏳</div>
<p style="font-size: 18px;">Carregando ${file.name}...</p>
</div>
`;
try {
// Load CSV
const data = await loadCSV(file.filename);
// Update state
currentCSVData = data;
currentFilename = file.filename;
currentFileId = fileId;
// Render table
renderCSVTable(data, file);
// Show buttons
document.getElementById('btnAddRecord').style.display = 'inline-block';
document.getElementById('btnDownload').style.display = 'inline-block';
} catch (error) {
document.getElementById('csvContent').innerHTML = `
<div style="text-align: center; padding: 60px 20px;">
<div style="font-size: 48px; margin-bottom: 16px; color: var(--color-error);">❌</div>
<p style="font-size: 18px; color: var(--color-error); margin-bottom: 8px;">Erro ao carregar arquivo</p>
<p style="font-size: 14px; color: var(--color-text-secondary);">${error.message}</p>
<button class="btn btn-primary" onclick="location.reload()" style="margin-top: 20px;">Recarregar Página</button>
</div>
`;
}
}
/**
* Render CSV data as table
* @param {Array<object>} data - CSV data
* @param {object} file - File metadata
*/
function renderCSVTable(data, file) {
if (data.length === 0) {
document.getElementById('csvContent').innerHTML = `
<div style="text-align: center; padding: 60px 20px;">
<div style="font-size: 48px; margin-bottom: 16px;">📭</div>
<p style="font-size: 18px; margin-bottom: 8px;">Arquivo vazio</p>
<p style="font-size: 14px; color: var(--color-text-secondary);">Adicione o primeiro registro</p>
</div>
`;
return;
}
const headers = Object.keys(data[0]);
let html = `
<div style="margin-bottom: 20px; padding: 16px; background: var(--color-bg-1); border-radius: 8px;">
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 8px;">
<span style="font-size: 32px;">${file.icon}</span>
<div>
<h3 style="margin: 0; font-size: 18px;">${file.name}</h3>
<p style="margin: 0; font-size: 13px; color: var(--color-text-secondary);">${file.description}</p>
</div>
</div>
<div style="display: flex; gap: 20px; margin-top: 12px; font-size: 13px; color: var(--color-text-secondary);">
<span>📄 <strong>${data.length}</strong> registros</span>
<span>📊 <strong>${headers.length}</strong> colunas</span>
<span>💾 <strong>${currentFilename}</strong></span>
</div>
</div>
<div class="table-wrapper" style="overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse; font-size: 13px;">
<thead>
<tr style="background: var(--color-primary); color: white;">
<th style="padding: 12px; text-align: left; position: sticky; left: 0; background: var(--color-primary);">#</th>
${headers.map(h => `<th style="padding: 12px; text-align: left; white-space: nowrap;">${h}</th>`).join('')}
<th style="padding: 12px; text-align: center; width: 120px;">Ações</th>
</tr>
</thead>
<tbody>
${data.map((row, index) => `
<tr style="border-bottom: 1px solid var(--color-border); ${index % 2 === 0 ? 'background: var(--color-surface);' : ''}">
<td style="padding: 12px; font-weight: bold; position: sticky; left: 0; background: ${index % 2 === 0 ? 'var(--color-surface)' : 'var(--color-background)'};">${index + 1}</td>
${headers.map(h => `<td style="padding: 12px; white-space: nowrap;">${row[h] || '-'}</td>`).join('')}
<td style="padding: 12px; text-align: center;">
<button class="btn-icon-small" onclick="editRecord(${index})" title="Editar" style="background: var(--color-primary); color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; margin-right: 4px;">✏️</button>
<button class="btn-icon-small" onclick="deleteRecord(${index})" title="Deletar" style="background: var(--color-error); color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer;">🗑️</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
document.getElementById('csvContent').innerHTML = html;
}
/**
* Add new record
*/
export function addNewRecord() {
if (currentCSVData.length === 0) {
alert('⚠️ Não é possível adicionar registro em arquivo vazio. Faça upload de um arquivo com estrutura.');
return;
}
editingIndex = -1;
const headers = Object.keys(currentCSVData[0]);
showRecordModal(' Adicionar Novo Registro', headers, {});
}
/**
* Edit existing record
* @param {number} index - Record index
*/
export function editRecord(index) {
editingIndex = index;
const record = currentCSVData[index];
const headers = Object.keys(record);
showRecordModal(`✏️ Editar Registro #${index + 1}`, headers, record);
}
/**
* Delete record
* @param {number} index - Record index
*/
export function deleteRecord(index) {
const record = currentCSVData[index];
const recordName = record.nome || record.id || `Registro #${index + 1}`;
if (!confirm(`🗑️ Tem certeza que deseja deletar:\n\n"${recordName}"?\n\nEsta ação não pode ser desfeita.`)) {
return;
}
// Remove from array
currentCSVData.splice(index, 1);
// Re-render table
const files = getAvailableCSVFiles();
const file = files.find(f => f.id === currentFileId);
renderCSVTable(currentCSVData, file);
// Show success message
showToast('✅ Registro deletado com sucesso!', 'success');
}
/**
* Show record editor modal
* @param {string} title - Modal title
* @param {Array<string>} headers - Field names
* @param {object} data - Current data
*/
function showRecordModal(title, headers, data) {
document.getElementById('recordModalTitle').textContent = title;
let html = '<div style="display: flex; flex-direction: column; gap: 16px;">';
headers.forEach(header => {
const value = data[header] || '';
html += `
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label">${header}</label>
<input
type="text"
class="form-control"
id="field_${header}"
value="${value}"
placeholder="Digite ${header}"
${header === 'id' ? 'required' : ''}
>
${header === 'id' ? '<small style="color: var(--color-text-secondary);">Campo obrigatório e único</small>' : ''}
</div>
`;
});
html += '</div>';
document.getElementById('recordModalBody').innerHTML = html;
document.getElementById('csvRecordModal').classList.add('active');
}
/**
* Close record editor modal
*/
export function closeRecordModal() {
document.getElementById('csvRecordModal').classList.remove('active');
editingIndex = -1;
}
/**
* Save record (add or edit)
*/
export function saveRecord() {
const headers = Object.keys(currentCSVData[0] || {});
const newRecord = {};
// Collect form data
headers.forEach(header => {
const input = document.getElementById(`field_${header}`);
if (input) {
newRecord[header] = input.value.trim();
}
});
// Validate required fields
if (!newRecord.id) {
alert('⚠️ O campo "id" é obrigatório!');
return;
}
// Check for duplicate ID (only when adding new)
if (editingIndex === -1) {
const duplicate = currentCSVData.find(r => r.id === newRecord.id);
if (duplicate) {
alert(`⚠️ Já existe um registro com o ID "${newRecord.id}"!\n\nEscolha um ID único.`);
return;
}
}
// Add or update
if (editingIndex === -1) {
// Add new
currentCSVData.push(newRecord);
showToast('✅ Registro adicionado com sucesso!', 'success');
} else {
// Update existing
currentCSVData[editingIndex] = newRecord;
showToast('✅ Registro atualizado com sucesso!', 'success');
}
// Re-render table
const files = getAvailableCSVFiles();
const file = files.find(f => f.id === currentFileId);
renderCSVTable(currentCSVData, file);
// Close modal
closeRecordModal();
}
/**
* Download current CSV
*/
export function downloadCurrentCSV() {
if (currentCSVData.length === 0) {
alert('⚠️ Não há dados para download!');
return;
}
// Validate data
const validation = validateCSVData(currentCSVData);
if (!validation.valid) {
const proceed = confirm(`⚠️ Dados contêm erros:\n\n${validation.errors.join('\n')}\n\nDeseja fazer download mesmo assim?`);
if (!proceed) return;
}
// Convert to CSV
const csvText = toCSV(currentCSVData);
// Download
downloadCSV(currentFilename, csvText);
showToast('💾 CSV baixado com sucesso!', 'success');
}
/**
* Show toast notification
* @param {string} message - Message text
* @param {string} type - Type (success, error, warning)
*/
function showToast(message, type = 'success') {
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
toast.style.cssText = `
position: fixed;
bottom: 24px;
right: 24px;
padding: 16px 24px;
background: ${type === 'success' ? 'var(--color-success)' : 'var(--color-error)'};
color: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 10000;
font-size: 14px;
font-weight: 500;
opacity: 0;
transform: translateY(20px);
transition: all 0.3s ease;
`;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '1';
toast.style.transform = 'translateY(0)';
}, 10);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateY(20px)';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// Export to global scope for onclick handlers
if (typeof window !== 'undefined') {
window.openCSVManager = openCSVManager;
window.closeCSVManager = closeCSVManager;
window.loadSelectedCSV = loadSelectedCSV;
window.addNewRecord = addNewRecord;
window.editRecord = editRecord;
window.deleteRecord = deleteRecord;
window.closeRecordModal = closeRecordModal;
window.saveRecord = saveRecord;
window.downloadCurrentCSV = downloadCurrentCSV;
}