414 lines
17 KiB
JavaScript
414 lines
17 KiB
JavaScript
/**
|
||
* HistoryManager – Frontend for conversion history
|
||
* Renders paginated table, detail view, stats, and filters.
|
||
*/
|
||
window.EDIBridge = window.EDIBridge || {};
|
||
|
||
class HistoryManager {
|
||
constructor(app) {
|
||
this.app = app;
|
||
this.currentPage = 1;
|
||
this.pageSize = 20;
|
||
this.statusFilter = 'ALL';
|
||
this.dateFilter = 'ALL';
|
||
this.searchTerm = '';
|
||
this.totalRows = 0;
|
||
}
|
||
|
||
/**
|
||
* Called when the History page becomes visible.
|
||
*/
|
||
async activate() {
|
||
await this.loadStats();
|
||
await this.loadTable();
|
||
}
|
||
|
||
/**
|
||
* Load and render statistics cards.
|
||
*/
|
||
async loadStats() {
|
||
if (!window.electronAPI) return;
|
||
const stats = await window.electronAPI.dbGetStats();
|
||
const container = document.getElementById('historyStats');
|
||
if (!container) return;
|
||
|
||
container.innerHTML = `
|
||
<div class="stat-card ${this.statusFilter === 'ALL' && this.dateFilter === 'ALL' ? 'active' : ''}"
|
||
onclick="app.historyManager.setFilter('ALL')" style="cursor:pointer;">
|
||
<div class="stat-value">${stats.total}</div>
|
||
<div class="stat-label">Gesamt</div>
|
||
</div>
|
||
<div class="stat-card ${this.statusFilter === 'ERFOLGREICH' ? 'active' : ''}"
|
||
onclick="app.historyManager.setFilter('ERFOLGREICH')" style="cursor:pointer; border-color: var(--success);">
|
||
<div class="stat-value" style="color: var(--success);">${stats.success}</div>
|
||
<div class="stat-label">Erfolgreich</div>
|
||
</div>
|
||
<div class="stat-card ${this.statusFilter === 'FEHLERHAFT' ? 'active' : ''}"
|
||
onclick="app.historyManager.setFilter('FEHLERHAFT')" style="cursor:pointer; border-color: var(--danger);">
|
||
<div class="stat-value" style="color: var(--danger);">${stats.error}</div>
|
||
<div class="stat-label">Fehlerhaft</div>
|
||
</div>
|
||
<div class="stat-card ${this.dateFilter === 'TODAY' ? 'active' : ''}"
|
||
onclick="app.historyManager.setDateFilter('TODAY')" style="cursor:pointer;">
|
||
<div class="stat-value" style="color: var(--accent);">${stats.today}</div>
|
||
<div class="stat-label">Heute</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* Load and render the conversion table.
|
||
*/
|
||
async loadTable() {
|
||
if (!window.electronAPI) return;
|
||
|
||
const offset = (this.currentPage - 1) * this.pageSize;
|
||
const params = {
|
||
limit: this.pageSize,
|
||
offset,
|
||
statusFilter: this.statusFilter,
|
||
dateFilter: this.dateFilter,
|
||
search: this.searchTerm
|
||
};
|
||
console.log('[History] Loading table with params:', params);
|
||
const result = await window.electronAPI.dbGetAll(params);
|
||
|
||
this.totalRows = result.total;
|
||
const totalPages = Math.max(1, Math.ceil(result.total / this.pageSize));
|
||
|
||
const tbody = document.getElementById('historyTableBody');
|
||
const pagination = document.getElementById('historyPagination');
|
||
if (!tbody) return;
|
||
|
||
if (result.rows.length === 0) {
|
||
tbody.innerHTML = `<tr><td colspan="6" style="text-align:center; padding:40px; color: var(--text-dim); font-style:italic;">Keine Einträge gefunden.</td></tr>`;
|
||
} else {
|
||
tbody.innerHTML = result.rows.map(row => {
|
||
const ts = this._formatTimestamp(row.zeitstempel);
|
||
const statusIcon = row.status === 'ERFOLGREICH'
|
||
? '<span style="color: var(--success);">✅</span>'
|
||
: '<span style="color: var(--danger);">❌</span>';
|
||
const modus = this._shortModus(row.konvertierungsmodus, row.quellformat, row.zielformat);
|
||
const source = row.quelle === 'WATCHER'
|
||
? '<span class="badge badge-config" style="font-size:0.65rem;">AUTO</span>'
|
||
: '<span class="badge badge-edi" style="font-size:0.65rem;">GUI</span>';
|
||
|
||
return `
|
||
<tr class="history-row" onclick="app.historyManager.showDetail(${row.id})" style="cursor:pointer;">
|
||
<td style="color: var(--text-dim); font-size:0.8rem;">${row.id}</td>
|
||
<td>${ts}</td>
|
||
<td>
|
||
<div style="font-weight:600;">${this._escapeHtml(row.dateiname)}</div>
|
||
${row.ausgangsdateiname ? `<div style="font-size:0.75rem; color:var(--text-dim);">→ ${this._escapeHtml(row.ausgangsdateiname)}</div>` : ''}
|
||
</td>
|
||
<td>${modus}</td>
|
||
<td>${statusIcon} ${source}</td>
|
||
<td>
|
||
<button class="btn-secondary" style="padding:4px 8px; font-size:0.7rem;"
|
||
onclick="event.stopPropagation(); app.historyManager.deleteEntry(${row.id})">
|
||
<i data-lucide="trash-2" style="width:12px;height:12px;"></i>
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
if (pagination) {
|
||
pagination.innerHTML = `
|
||
<button class="btn-secondary" ${this.currentPage <= 1 ? 'disabled' : ''}
|
||
onclick="app.historyManager.goToPage(${this.currentPage - 1})">
|
||
<i data-lucide="chevron-left" style="width:14px;height:14px;"></i> Zurück
|
||
</button>
|
||
<span style="color: var(--text-dim); font-size:0.85rem;">
|
||
Seite ${this.currentPage} von ${totalPages} (${result.total} Einträge)
|
||
</span>
|
||
<button class="btn-secondary" ${this.currentPage >= totalPages ? 'disabled' : ''}
|
||
onclick="app.historyManager.goToPage(${this.currentPage + 1})">
|
||
Weiter <i data-lucide="chevron-right" style="width:14px;height:14px;"></i>
|
||
</button>
|
||
`;
|
||
}
|
||
|
||
// Re-render icons
|
||
if (window.lucide) {
|
||
try { lucide.createIcons(); } catch (e) { }
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Show detail view for a single conversion.
|
||
*/
|
||
async showDetail(id) {
|
||
if (!window.electronAPI) return;
|
||
const record = await window.electronAPI.dbGetById(id);
|
||
if (!record) return;
|
||
|
||
const modal = document.getElementById('historyDetailModal');
|
||
const content = document.getElementById('historyDetailContent');
|
||
if (!modal || !content) return;
|
||
|
||
const statusBadge = record.status === 'ERFOLGREICH'
|
||
? '<span style="color:var(--success); font-weight:700;">✅ ERFOLGREICH</span>'
|
||
: '<span style="color:var(--danger); font-weight:700;">❌ FEHLERHAFT</span>';
|
||
|
||
content.innerHTML = `
|
||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
|
||
<h3 style="margin:0; font-size:1.1rem;">Konvertierung #${record.id}</h3>
|
||
<button class="btn-secondary" onclick="app.historyManager.closeDetail()">
|
||
<i data-lucide="x" style="width:14px;height:14px;"></i> Schließen
|
||
</button>
|
||
</div>
|
||
|
||
<div class="history-detail-grid">
|
||
<div class="history-detail-item">
|
||
<span class="label">Zeitstempel</span>
|
||
<strong>${this._formatTimestamp(record.zeitstempel)}</strong>
|
||
</div>
|
||
<div class="history-detail-item">
|
||
<span class="label">Status</span>
|
||
${statusBadge}
|
||
</div>
|
||
<div class="history-detail-item">
|
||
<span class="label">Eingangsdatei</span>
|
||
<strong>${this._escapeHtml(record.dateiname)}</strong>
|
||
</div>
|
||
<div class="history-detail-item">
|
||
<span class="label">Ausgangsdatei</span>
|
||
<strong>${this._escapeHtml(record.ausgangsdateiname || '-')}</strong>
|
||
</div>
|
||
<div class="history-detail-item">
|
||
<span class="label">Quellformat</span>
|
||
<strong>${this._escapeHtml(record.quellformat || '-')}</strong>
|
||
</div>
|
||
<div class="history-detail-item">
|
||
<span class="label">Zielformat</span>
|
||
<strong>${this._escapeHtml(record.zielformat || '-')}</strong>
|
||
</div>
|
||
<div class="history-detail-item">
|
||
<span class="label">Konvertierungsmodus</span>
|
||
<strong>${this._escapeHtml(record.konvertierungsmodus || '-')}</strong>
|
||
</div>
|
||
<div class="history-detail-item">
|
||
<span class="label">Kunden-ID</span>
|
||
<strong>${this._escapeHtml(record.kunden_id || '-')}</strong>
|
||
</div>
|
||
<div class="history-detail-item">
|
||
<span class="label">Quelle</span>
|
||
<strong>${record.quelle === 'WATCHER' ? '🤖 Automatisch (Watcher)' : '👤 Manuell (GUI)'}</strong>
|
||
</div>
|
||
</div>
|
||
|
||
${record.fehlermeldung ? `
|
||
<div style="background: rgba(239,68,68,0.1); border:1px solid rgba(239,68,68,0.3); border-radius:8px; padding:12px; margin:15px 0; color: #fca5a5;">
|
||
<strong>Fehlermeldung:</strong><br>
|
||
${this._escapeHtml(record.fehlermeldung)}
|
||
</div>
|
||
` : ''}
|
||
|
||
<div class="history-detail-tabs">
|
||
<button class="btn-secondary history-tab-btn active" data-tab="eingang" onclick="app.historyManager.switchTab('eingang')">
|
||
📄 Eingangsdaten
|
||
</button>
|
||
<button class="btn-secondary history-tab-btn" data-tab="ausgang" onclick="app.historyManager.switchTab('ausgang')">
|
||
📄 Ausgangsdaten
|
||
</button>
|
||
${record.ausgang_daten ? `
|
||
<button class="btn-secondary" onclick="app.historyManager.openInViewer(${record.id})">
|
||
<i data-lucide="eye" style="width:14px;height:14px;"></i> Im Viewer öffnen
|
||
</button>
|
||
` : ''}
|
||
</div>
|
||
|
||
<div id="historyTabEingang" class="history-tab-content">
|
||
<pre style="max-height:400px; overflow:auto; font-size:0.8rem;">${this._escapeHtml(record.eingang_daten || 'Keine Eingangsdaten gespeichert.')}</pre>
|
||
</div>
|
||
<div id="historyTabAusgang" class="history-tab-content" style="display:none;">
|
||
<pre style="max-height:400px; overflow:auto; font-size:0.8rem;">${this._escapeHtml(record.ausgang_daten || 'Keine Ausgangsdaten gespeichert.')}</pre>
|
||
</div>
|
||
`;
|
||
|
||
modal.style.display = 'flex';
|
||
|
||
if (window.lucide) {
|
||
try { lucide.createIcons(); } catch (e) { }
|
||
}
|
||
}
|
||
|
||
switchTab(tab) {
|
||
const eingang = document.getElementById('historyTabEingang');
|
||
const ausgang = document.getElementById('historyTabAusgang');
|
||
const buttons = document.querySelectorAll('.history-tab-btn');
|
||
|
||
buttons.forEach(btn => {
|
||
btn.classList.toggle('active', btn.dataset.tab === tab);
|
||
});
|
||
|
||
if (tab === 'eingang') {
|
||
if (eingang) eingang.style.display = 'block';
|
||
if (ausgang) ausgang.style.display = 'none';
|
||
} else {
|
||
if (eingang) eingang.style.display = 'none';
|
||
if (ausgang) ausgang.style.display = 'block';
|
||
}
|
||
}
|
||
|
||
async openInViewer(id) {
|
||
const record = await window.electronAPI.dbGetById(id);
|
||
if (!record || !record.ausgang_daten) return;
|
||
|
||
this.closeDetail();
|
||
// Switch to viewer page and process the content
|
||
if (this.app) {
|
||
this.app.showPage('viewer');
|
||
if (this.app.viewer) {
|
||
this.app.viewer.processContent(record.ausgang_daten);
|
||
}
|
||
}
|
||
}
|
||
|
||
closeDetail() {
|
||
const modal = document.getElementById('historyDetailModal');
|
||
if (modal) modal.style.display = 'none';
|
||
}
|
||
|
||
async deleteEntry(id) {
|
||
if (!confirm('Eintrag wirklich löschen?')) return;
|
||
await window.electronAPI.dbDelete(id);
|
||
await this.loadStats();
|
||
await this.loadTable();
|
||
}
|
||
|
||
goToPage(page) {
|
||
if (page < 1) return;
|
||
this.currentPage = page;
|
||
this.loadTable();
|
||
}
|
||
|
||
setFilter(status) {
|
||
this.statusFilter = status;
|
||
this.dateFilter = 'ALL';
|
||
this.searchTerm = ''; // Clear search when picking a card
|
||
this.currentPage = 1;
|
||
|
||
// Reset search input
|
||
const searchInput = document.getElementById('historySearch');
|
||
if (searchInput) searchInput.value = '';
|
||
|
||
// Update active button state
|
||
document.querySelectorAll('.history-filter-btn').forEach(btn => {
|
||
btn.classList.toggle('active', btn.dataset.filter === status);
|
||
});
|
||
|
||
this.loadStats();
|
||
this.loadTable();
|
||
}
|
||
|
||
setDateFilter(dateType) {
|
||
this.dateFilter = dateType;
|
||
this.statusFilter = 'ALL'; // Reset status when picking date
|
||
this.searchTerm = ''; // Clear search when picking a card
|
||
this.currentPage = 1;
|
||
|
||
// Reset filter bar buttons
|
||
document.querySelectorAll('.history-filter-btn').forEach(btn => {
|
||
btn.classList.toggle('active', false);
|
||
});
|
||
|
||
// Reset search input
|
||
const searchInput = document.getElementById('historySearch');
|
||
if (searchInput) searchInput.value = '';
|
||
|
||
this.loadStats();
|
||
this.loadTable();
|
||
}
|
||
|
||
onSearch(value) {
|
||
this.searchTerm = value;
|
||
this.currentPage = 1;
|
||
// Debounce
|
||
clearTimeout(this._searchTimer);
|
||
this._searchTimer = setTimeout(() => this.loadTable(), 300);
|
||
}
|
||
|
||
async exportCSV() {
|
||
if (!window.electronAPI) return;
|
||
|
||
// Fetch all rows (no pagination)
|
||
const result = await window.electronAPI.dbGetAll({
|
||
limit: 99999,
|
||
offset: 0,
|
||
statusFilter: this.statusFilter,
|
||
search: this.searchTerm
|
||
});
|
||
|
||
if (!result.rows || result.rows.length === 0) {
|
||
alert('Keine Daten zum Exportieren.');
|
||
return;
|
||
}
|
||
|
||
const headers = ['ID', 'Zeitstempel', 'Dateiname', 'Ausgangsdatei', 'Quellformat', 'Zielformat', 'Modus', 'Kunden-ID', 'Status', 'Fehlermeldung', 'Quelle'];
|
||
const csvRows = [headers.join(';')];
|
||
|
||
result.rows.forEach(row => {
|
||
csvRows.push([
|
||
row.id,
|
||
row.zeitstempel,
|
||
`"${(row.dateiname || '').replace(/"/g, '""')}"`,
|
||
`"${(row.ausgangsdateiname || '').replace(/"/g, '""')}"`,
|
||
row.quellformat || '',
|
||
row.zielformat || '',
|
||
row.konvertierungsmodus || '',
|
||
row.kunden_id || '',
|
||
row.status,
|
||
`"${(row.fehlermeldung || '').replace(/"/g, '""')}"`,
|
||
row.quelle
|
||
].join(';'));
|
||
});
|
||
|
||
const csv = csvRows.join('\n');
|
||
const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8;' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `konvertierungen_${new Date().toISOString().slice(0, 10)}.csv`;
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
|
||
// ─── Helpers ─────────────────────────────────────────────────────
|
||
|
||
_formatTimestamp(ts) {
|
||
if (!ts) return '-';
|
||
try {
|
||
const d = new Date(ts.replace(' ', 'T'));
|
||
return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })
|
||
+ ' ' + d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||
} catch (e) {
|
||
return ts;
|
||
}
|
||
}
|
||
|
||
_shortModus(modus, quellformat, zielformat) {
|
||
if (quellformat && zielformat) {
|
||
// Shorten for display
|
||
const qf = quellformat.replace('EDIFACT ', '').replace('D04A', '').trim();
|
||
const zf = zielformat.replace('EDIFACT ', '').trim();
|
||
return `<span style="font-size:0.8rem;">${this._escapeHtml(qf)} → ${this._escapeHtml(zf)}</span>`;
|
||
}
|
||
if (modus) return `<span style="font-size:0.8rem;">${this._escapeHtml(modus)}</span>`;
|
||
return '<span style="color:var(--text-dim);">-</span>';
|
||
}
|
||
|
||
_escapeHtml(str) {
|
||
if (!str) return '';
|
||
return String(str)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"');
|
||
}
|
||
}
|
||
|
||
window.EDIBridge.HistoryManager = HistoryManager;
|