working on new system for importing
This commit is contained in:
@@ -1,314 +0,0 @@
|
||||
(function() {
|
||||
const apiBase = '/api/flexible';
|
||||
let state = {
|
||||
fileType: '',
|
||||
targetTable: '',
|
||||
q: '',
|
||||
skip: 0,
|
||||
limit: 50,
|
||||
total: 0,
|
||||
hasKeys: [],
|
||||
};
|
||||
|
||||
function q(id) { return document.getElementById(id); }
|
||||
|
||||
function formatPreviewHtml(obj, term) {
|
||||
// Returns sanitized HTML with clickable keys
|
||||
try {
|
||||
const payload = obj && obj.unmapped && typeof obj.unmapped === 'object' ? obj.unmapped : obj;
|
||||
const keys = Object.keys(payload || {}).slice(0, 5);
|
||||
const segments = keys.map((k) => {
|
||||
const safeKey = window.htmlSanitizer.escape(String(k));
|
||||
const valueStr = String(payload[k]).slice(0, 60);
|
||||
const valueHtml = term && term.trim().length > 0 ? highlight(valueStr, term) : window.htmlSanitizer.escape(valueStr);
|
||||
return `<span class="kv-pair"><button type="button" class="key-link text-primary-700 dark:text-primary-400 hover:underline" data-key="${safeKey}">${safeKey}</button>: ${valueHtml}</span>`;
|
||||
});
|
||||
return segments.join(', ');
|
||||
} catch (_) { return ''; }
|
||||
}
|
||||
|
||||
function escapeRegExp(str) {
|
||||
return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
function highlight(text, term) {
|
||||
if (!term) return window.htmlSanitizer.escape(text);
|
||||
const pattern = new RegExp(escapeRegExp(term), 'ig');
|
||||
const escaped = window.htmlSanitizer.escape(text);
|
||||
// Replace on the escaped string to avoid breaking HTML
|
||||
return escaped.replace(pattern, (m) => `<mark>${window.htmlSanitizer.escape(m)}</mark>`);
|
||||
}
|
||||
|
||||
async function loadOptions() {
|
||||
try {
|
||||
const res = await window.http.wrappedFetch(`${apiBase}/options`);
|
||||
if (!res.ok) throw await window.http.toError(res, 'Failed to load options');
|
||||
const data = await res.json();
|
||||
const fileSel = q('filterFileType');
|
||||
const tableSel = q('filterTargetTable');
|
||||
// Clear existing except first
|
||||
fileSel.length = 1; tableSel.length = 1;
|
||||
(data.file_types || []).forEach(v => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = v; opt.textContent = v; fileSel.appendChild(opt);
|
||||
});
|
||||
(data.target_tables || []).forEach(v => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = v; opt.textContent = v; tableSel.appendChild(opt);
|
||||
});
|
||||
} catch (e) {
|
||||
alert(window.http.formatAlert(e, 'Error loading options'));
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRows() {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (state.fileType) params.set('file_type', state.fileType);
|
||||
if (state.targetTable) params.set('target_table', state.targetTable);
|
||||
if (state.q) params.set('q', state.q);
|
||||
if (Array.isArray(state.hasKeys)) {
|
||||
state.hasKeys.forEach((k) => {
|
||||
if (k && String(k).trim().length > 0) params.append('has_keys', String(k).trim());
|
||||
});
|
||||
}
|
||||
params.set('skip', String(state.skip));
|
||||
params.set('limit', String(state.limit));
|
||||
const res = await window.http.wrappedFetch(`${apiBase}/imports?${params.toString()}`);
|
||||
if (!res.ok) throw await window.http.toError(res, 'Failed to load flexible imports');
|
||||
const data = await res.json();
|
||||
state.total = data.total || 0;
|
||||
renderRows(data.items || []);
|
||||
renderMeta();
|
||||
renderKeyChips();
|
||||
} catch (e) {
|
||||
alert(window.http.formatAlert(e, 'Error loading flexible imports'));
|
||||
}
|
||||
}
|
||||
|
||||
function renderRows(items) {
|
||||
const tbody = q('flexibleRows');
|
||||
tbody.innerHTML = '';
|
||||
items.forEach(item => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.className = 'hover:bg-neutral-50 dark:hover:bg-neutral-700/40 cursor-pointer';
|
||||
tr.innerHTML = `
|
||||
<td class="px-3 py-2 whitespace-nowrap">${item.id}</td>
|
||||
<td class="px-3 py-2 whitespace-nowrap">${window.htmlSanitizer.escape(item.file_type || '')}</td>
|
||||
<td class="px-3 py-2 whitespace-nowrap">${window.htmlSanitizer.escape(item.target_table || '')}</td>
|
||||
<td class="px-3 py-2 whitespace-nowrap text-xs text-neutral-500">${window.htmlSanitizer.escape((item.primary_key_field || '') + (item.primary_key_value ? '=' + item.primary_key_value : ''))}</td>
|
||||
<td class="px-3 py-2 text-xs previewCell"></td>
|
||||
<td class="px-3 py-2 text-right">
|
||||
<button class="inline-flex items-center gap-1 text-primary-600 hover:text-primary-700 px-2 py-1 text-xs rounded-md hover:bg-primary-50 dark:hover:bg-primary-900/30" data-action="export" data-id="${item.id}">
|
||||
<i class="fa-solid fa-download"></i>
|
||||
<span>CSV</span>
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
// Set sanitized highlighted preview
|
||||
const previewCell = tr.querySelector('.previewCell');
|
||||
const previewHtml = formatPreviewHtml(item.extra_data || {}, state.q);
|
||||
window.setSafeHTML(previewCell, previewHtml);
|
||||
// Bind click on keys to add filters
|
||||
previewCell.querySelectorAll('.key-link').forEach((btn) => {
|
||||
btn.addEventListener('click', (ev) => {
|
||||
ev.stopPropagation();
|
||||
const key = btn.getAttribute('data-key') || '';
|
||||
addKeyFilter(key);
|
||||
});
|
||||
});
|
||||
// Row click opens modal
|
||||
tr.addEventListener('click', (ev) => {
|
||||
// Ignore clicks on the export button inside the row
|
||||
const target = ev.target.closest('button[data-action="export"]');
|
||||
if (target) return;
|
||||
openDetailModal(item);
|
||||
});
|
||||
// Export button handler
|
||||
tr.querySelector('button[data-action="export"]').addEventListener('click', (ev) => {
|
||||
ev.stopPropagation();
|
||||
exportSingleRow(item.id);
|
||||
});
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
function renderMeta() {
|
||||
const start = state.total === 0 ? 0 : state.skip + 1;
|
||||
const end = Math.min(state.skip + state.limit, state.total);
|
||||
q('rowsMeta').textContent = `Showing ${start}-${end} of ${state.total}`;
|
||||
q('prevPageBtn').disabled = state.skip === 0;
|
||||
q('nextPageBtn').disabled = state.skip + state.limit >= state.total;
|
||||
}
|
||||
|
||||
function applyFilters() {
|
||||
state.fileType = q('filterFileType').value || '';
|
||||
state.targetTable = q('filterTargetTable').value || '';
|
||||
state.q = (q('quickSearch').value || '').trim();
|
||||
state.skip = 0;
|
||||
loadRows();
|
||||
}
|
||||
|
||||
function addKeyFilter(key) {
|
||||
const k = String(key || '').trim();
|
||||
if (!k) return;
|
||||
if (!Array.isArray(state.hasKeys)) state.hasKeys = [];
|
||||
if (!state.hasKeys.includes(k)) {
|
||||
state.hasKeys.push(k);
|
||||
state.skip = 0;
|
||||
loadRows();
|
||||
}
|
||||
}
|
||||
|
||||
function removeKeyFilter(key) {
|
||||
const k = String(key || '').trim();
|
||||
if (!k) return;
|
||||
state.hasKeys = (state.hasKeys || []).filter((x) => x !== k);
|
||||
state.skip = 0;
|
||||
loadRows();
|
||||
}
|
||||
|
||||
function clearKeyFilters() {
|
||||
if ((state.hasKeys || []).length === 0) return;
|
||||
state.hasKeys = [];
|
||||
state.skip = 0;
|
||||
loadRows();
|
||||
}
|
||||
|
||||
function renderKeyChips() {
|
||||
const container = q('keyChipsContainer');
|
||||
const chipsWrap = q('keyChips');
|
||||
const clearBtn = q('clearKeyChips');
|
||||
if (!container || !chipsWrap) return;
|
||||
chipsWrap.innerHTML = '';
|
||||
const keys = state.hasKeys || [];
|
||||
if (keys.length === 0) {
|
||||
container.classList.add('hidden');
|
||||
} else {
|
||||
container.classList.remove('hidden');
|
||||
keys.forEach((k) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs bg-primary-50 text-primary-700 border border-primary-200 hover:bg-primary-100 dark:bg-primary-900/30 dark:text-primary-200 dark:border-primary-800';
|
||||
btn.setAttribute('data-chip-key', k);
|
||||
btn.innerHTML = `<span class="font-mono">${window.htmlSanitizer.escape(k)}</span> <i class="fa-solid fa-xmark"></i>`;
|
||||
btn.addEventListener('click', (ev) => {
|
||||
ev.stopPropagation();
|
||||
removeKeyFilter(k);
|
||||
});
|
||||
chipsWrap.appendChild(btn);
|
||||
});
|
||||
}
|
||||
if (clearBtn) {
|
||||
clearBtn.onclick = (ev) => { ev.preventDefault(); clearKeyFilters(); };
|
||||
}
|
||||
}
|
||||
|
||||
async function exportCsv() {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (state.fileType) params.set('file_type', state.fileType);
|
||||
if (state.targetTable) params.set('target_table', state.targetTable);
|
||||
if (Array.isArray(state.hasKeys)) {
|
||||
state.hasKeys.forEach((k) => {
|
||||
if (k && String(k).trim().length > 0) params.append('has_keys', String(k).trim());
|
||||
});
|
||||
}
|
||||
const url = `${apiBase}/export?${params.toString()}`;
|
||||
const res = await window.http.wrappedFetch(url);
|
||||
if (!res.ok) throw await window.http.toError(res, 'Export failed');
|
||||
const blob = await res.blob();
|
||||
const a = document.createElement('a');
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
a.href = objectUrl;
|
||||
a.download = 'flexible_unmapped.csv';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(objectUrl), 1000);
|
||||
} catch (e) {
|
||||
alert(window.http.formatAlert(e, 'Error exporting CSV'));
|
||||
}
|
||||
}
|
||||
|
||||
async function exportSingleRow(rowId) {
|
||||
try {
|
||||
const res = await window.http.wrappedFetch(`${apiBase}/export/${rowId}`);
|
||||
if (!res.ok) throw await window.http.toError(res, 'Export failed');
|
||||
const blob = await res.blob();
|
||||
const a = document.createElement('a');
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
a.href = objectUrl;
|
||||
a.download = `flexible_row_${rowId}.csv`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(objectUrl), 1000);
|
||||
} catch (e) {
|
||||
alert(window.http.formatAlert(e, 'Error exporting row CSV'));
|
||||
}
|
||||
}
|
||||
|
||||
function openDetailModal(item) {
|
||||
// Populate fields
|
||||
q('detailRowId').textContent = `#${item.id}`;
|
||||
q('detailFileType').textContent = item.file_type || '';
|
||||
q('detailTargetTable').textContent = item.target_table || '';
|
||||
q('detailPkField').textContent = item.primary_key_field || '';
|
||||
q('detailPkValue').textContent = item.primary_key_value || '';
|
||||
try {
|
||||
const pretty = JSON.stringify(item.extra_data || {}, null, 2);
|
||||
q('detailJson').textContent = pretty;
|
||||
} catch (_) {
|
||||
q('detailJson').textContent = '';
|
||||
}
|
||||
const exportBtn = q('detailExportBtn');
|
||||
exportBtn.onclick = () => exportSingleRow(item.id);
|
||||
openModal('flexibleDetailModal');
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
q('applyFiltersBtn').addEventListener('click', applyFilters);
|
||||
q('exportCsvBtn').addEventListener('click', exportCsv);
|
||||
const clearBtn = q('clearKeyChips');
|
||||
if (clearBtn) clearBtn.addEventListener('click', (ev) => { ev.preventDefault(); clearKeyFilters(); });
|
||||
// Quick search with debounce
|
||||
const searchInput = q('quickSearch');
|
||||
let searchTimer = null;
|
||||
searchInput.addEventListener('input', () => {
|
||||
const value = searchInput.value || '';
|
||||
clearTimeout(searchTimer);
|
||||
searchTimer = setTimeout(() => {
|
||||
state.q = value.trim();
|
||||
state.skip = 0;
|
||||
loadRows();
|
||||
}, 300);
|
||||
});
|
||||
searchInput.addEventListener('keydown', (ev) => {
|
||||
if (ev.key === 'Enter') {
|
||||
ev.preventDefault();
|
||||
clearTimeout(searchTimer);
|
||||
state.q = (searchInput.value || '').trim();
|
||||
state.skip = 0;
|
||||
loadRows();
|
||||
}
|
||||
});
|
||||
q('prevPageBtn').addEventListener('click', () => {
|
||||
state.skip = Math.max(0, state.skip - state.limit);
|
||||
loadRows();
|
||||
});
|
||||
q('nextPageBtn').addEventListener('click', () => {
|
||||
if (state.skip + state.limit < state.total) {
|
||||
state.skip += state.limit;
|
||||
loadRows();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
bindEvents();
|
||||
loadOptions().then(loadRows);
|
||||
});
|
||||
})();
|
||||
|
||||
|
||||
@@ -199,14 +199,25 @@ function initializeBatchProgressUI() {
|
||||
|
||||
async function cancelBatch(batchId) {
|
||||
try {
|
||||
const resp = await window.http.wrappedFetch(`/api/billing/statements/batch-progress/${encodeURIComponent(batchId)}`, { method: 'DELETE' });
|
||||
if (!resp.ok) {
|
||||
throw await window.http.toError(resp, 'Failed to cancel batch');
|
||||
if (!confirm(`Are you sure you want to cancel batch ${batchId}?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Import functionality removed
|
||||
|
||||
const result = await resp.json();
|
||||
console.log('Import batch cancelled:', result.message);
|
||||
|
||||
// Let stream update the row; no-op here
|
||||
// The progress will be updated via WebSocket
|
||||
} catch (e) {
|
||||
console.warn('Cancel failed', e);
|
||||
try { alert(window.http.formatAlert(e, 'Cancel failed')); } catch (_) {}
|
||||
console.warn('Cancel import batch failed', e);
|
||||
try {
|
||||
const errorMsg = window.http.formatAlert(e, 'Cancel import batch failed');
|
||||
alert(errorMsg);
|
||||
} catch (_) {
|
||||
alert('Failed to cancel import batch');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,14 +475,7 @@ async function checkUserPermissions() {
|
||||
const adminDivider = document.getElementById('admin-menu-divider');
|
||||
if (adminItem) adminItem.classList.remove('hidden');
|
||||
if (adminDivider) adminDivider.classList.remove('hidden');
|
||||
const importDesktop = document.getElementById('nav-import-desktop');
|
||||
const importMobile = document.getElementById('nav-import-mobile');
|
||||
if (importDesktop) importDesktop.classList.remove('hidden');
|
||||
if (importMobile) importMobile.classList.remove('hidden');
|
||||
const flexibleDesktop = document.getElementById('nav-flexible-desktop');
|
||||
const flexibleMobile = document.getElementById('nav-flexible-mobile');
|
||||
if (flexibleDesktop) flexibleDesktop.classList.remove('hidden');
|
||||
if (flexibleMobile) flexibleMobile.classList.remove('hidden');
|
||||
// Import navigation items removed
|
||||
}
|
||||
const userDropdownName = document.querySelector('#userDropdown button span');
|
||||
if (user.full_name && userDropdownName) {
|
||||
|
||||
Reference in New Issue
Block a user