Beide Module wurden aus einem anderen Kundenprojekt kopiert und bereinigt: Knowledgecenter: - Bild-Cache (1.985 Dateien) geleert, Ordnerstruktur mit .gitkeep behalten - Files-Gallery-Lizenzschlüssel entfernt (config.php) - Kundenspezifische Kategorien gelöscht: Teil I/II/III QM-Struktur, nummerierte QM-Kapitel, Gremien (Präsidium/Finanzausschuss/Landesausschuss), Sitzungsservice-Serie, Protokoll-Abteilungen, Testeinträge - 56 generische Kategorien bleiben (Downloads, Onboarding, Personalthemen …) Tasks: - Alle Betriebsdaten gelöscht (114 Tasks, 914 Submissions, 579 Assignments) - Task-Definitionen und Verlinkungen geleert - Kategoriestruktur bleibt (Allgemein, IT, Personal, Buchhaltung …) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
604 lines
22 KiB
JavaScript
604 lines
22 KiB
JavaScript
// ── CSV-Export Hilfsfunktionen (global) ──────────────────────────
|
|
var TaskCertCsvExport = (function () {
|
|
'use strict';
|
|
|
|
function escapeCsvCell(val) {
|
|
var str = String(val == null ? '' : val);
|
|
if (str.indexOf('"') !== -1 || str.indexOf(';') !== -1 || str.indexOf('\n') !== -1) {
|
|
return '"' + str.replace(/"/g, '""') + '"';
|
|
}
|
|
return str;
|
|
}
|
|
|
|
function buildCsv(headers, rows) {
|
|
var lines = [headers.map(escapeCsvCell).join(';')];
|
|
for (var i = 0; i < rows.length; i++) {
|
|
lines.push(rows[i].map(escapeCsvCell).join(';'));
|
|
}
|
|
return lines.join('\r\n');
|
|
}
|
|
|
|
function download(csvString, filename) {
|
|
var bom = '\uFEFF';
|
|
var blob = new Blob([bom + csvString], { type: 'text/csv;charset=utf-8;' });
|
|
var url = URL.createObjectURL(blob);
|
|
var link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = filename;
|
|
link.style.display = 'none';
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
return { escapeCsvCell: escapeCsvCell, buildCsv: buildCsv, download: download };
|
|
})();
|
|
|
|
// ══════════════════════════════════════════════════════════════════
|
|
// Übersicht-Grid
|
|
// ══════════════════════════════════════════════════════════════════
|
|
(function () {
|
|
'use strict';
|
|
|
|
var container = document.getElementById('task-cert-overview-grid');
|
|
if (!container) {
|
|
return;
|
|
}
|
|
|
|
var dataEl = document.getElementById('task-cert-overview-grid-data');
|
|
if (!dataEl) {
|
|
return;
|
|
}
|
|
|
|
var allRows = [];
|
|
try {
|
|
allRows = JSON.parse(dataEl.textContent || '[]');
|
|
} catch (e) {
|
|
container.textContent = 'Fehler beim Laden der Tabellendaten.';
|
|
return;
|
|
}
|
|
|
|
if (!window.gridjs || !window.gridjs.Grid) {
|
|
container.textContent = 'Grid.js konnte nicht geladen werden.';
|
|
return;
|
|
}
|
|
|
|
var html = gridjs.html;
|
|
var statusFilterEl = document.getElementById('task-cert-overview-status-filter');
|
|
var searchInputEl = document.getElementById('task-cert-overview-search');
|
|
var initialStatusFilter = container.getAttribute('data-initial-status-filter') || '';
|
|
|
|
// ── Hilfsfunktionen ──────────────────────────────────────────
|
|
|
|
function escapeHtml(str) {
|
|
if (!str) { return ''; }
|
|
var div = document.createElement('div');
|
|
div.appendChild(document.createTextNode(String(str)));
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function stripHtmlTags(val) {
|
|
if (!val) { return ''; }
|
|
if (typeof val === 'object' && val.props && val.props.content) {
|
|
var tmp = document.createElement('div');
|
|
tmp.innerHTML = val.props.content;
|
|
return (tmp.textContent || tmp.innerText || '').trim();
|
|
}
|
|
return String(val);
|
|
}
|
|
|
|
function formatDateDisplay(isoValue) {
|
|
if (!isoValue) { return '-'; }
|
|
try {
|
|
var d = new Date(isoValue);
|
|
if (isNaN(d.getTime())) { return isoValue; }
|
|
var day = ('0' + d.getDate()).slice(-2);
|
|
var month = ('0' + (d.getMonth() + 1)).slice(-2);
|
|
var year = d.getFullYear();
|
|
var hours = ('0' + d.getHours()).slice(-2);
|
|
var minutes = ('0' + d.getMinutes()).slice(-2);
|
|
return day + '.' + month + '.' + year + ' ' + hours + ':' + minutes;
|
|
} catch (e) {
|
|
return isoValue;
|
|
}
|
|
}
|
|
|
|
// ── Sort-Comparators ─────────────────────────────────────────
|
|
|
|
function compareHtmlText(a, b) {
|
|
var ta = stripHtmlTags(a);
|
|
var tb = stripHtmlTags(b);
|
|
return ta.localeCompare(tb, 'de');
|
|
}
|
|
|
|
function compareDateValues(a, b) {
|
|
var sa = (a && a._sort) || '';
|
|
var sb = (b && b._sort) || '';
|
|
if (sa === sb) { return 0; }
|
|
if (sa === '') { return 1; }
|
|
if (sb === '') { return -1; }
|
|
return sa < sb ? -1 : 1;
|
|
}
|
|
|
|
function compareNumeric(a, b) {
|
|
var ta = stripHtmlTags(a);
|
|
var tb = stripHtmlTags(b);
|
|
var na = ta === '-' || ta === '' ? -1 : parseFloat(ta) || 0;
|
|
var nb = tb === '-' || tb === '' ? -1 : parseFloat(tb) || 0;
|
|
return na - nb;
|
|
}
|
|
|
|
function compareStrings(a, b) {
|
|
var sa = String(a || '');
|
|
var sb = String(b || '');
|
|
if (sa === '-') { sa = ''; }
|
|
if (sb === '-') { sb = ''; }
|
|
return sa.localeCompare(sb, 'de');
|
|
}
|
|
|
|
// ── Zell-Helfer ──────────────────────────────────────────────
|
|
|
|
function dateCell(isoValue) {
|
|
return {
|
|
_sort: isoValue || '',
|
|
_display: formatDateDisplay(isoValue)
|
|
};
|
|
}
|
|
|
|
function dateCellFormatter(cell) {
|
|
if (cell && typeof cell === 'object' && cell._display !== undefined) {
|
|
return cell._display;
|
|
}
|
|
return cell || '-';
|
|
}
|
|
|
|
// ── Suchtext-Cache ───────────────────────────────────────────
|
|
|
|
function buildSearchText(row) {
|
|
return [
|
|
row.contact_name || '',
|
|
row.contact_email || '',
|
|
row.status_label || '',
|
|
row.cycle_key || '',
|
|
row.certificate_label || '',
|
|
row.certificate_code || '',
|
|
formatDateDisplay(row.assigned_at_sort),
|
|
formatDateDisplay(row.due_at_sort),
|
|
formatDateDisplay(row.completed_at_sort),
|
|
formatDateDisplay(row.overdue_at_sort),
|
|
formatDateDisplay(row.last_activity_sort),
|
|
formatDateDisplay(row.valid_until_sort)
|
|
].join(' ').toLowerCase();
|
|
}
|
|
|
|
allRows.forEach(function (row) {
|
|
row._searchText = buildSearchText(row);
|
|
});
|
|
|
|
// ── Grid-Daten bauen ─────────────────────────────────────────
|
|
|
|
function buildGridData(rows) {
|
|
return rows.map(function (row) {
|
|
var nameHtml = '<span>' + escapeHtml(row.contact_name || '-') + '</span>';
|
|
if (row.contact_email) {
|
|
nameHtml += '<br><small>' + escapeHtml(row.contact_email) + '</small>';
|
|
}
|
|
|
|
var statusText = row.status_label || '-';
|
|
|
|
var escalationHtml = '-';
|
|
if (row.grace_badge_html) {
|
|
escalationHtml = row.grace_badge_html;
|
|
} else if (row.escalation_level !== null && row.escalation_level !== undefined && row.escalation_level !== '') {
|
|
escalationHtml = String(row.escalation_level);
|
|
}
|
|
|
|
var certHtml = '-';
|
|
if (row.certificate_label) {
|
|
certHtml = escapeHtml(row.certificate_label);
|
|
if (row.certificate_code) {
|
|
certHtml += '<br><small>' + escapeHtml(row.certificate_code) + '</small>';
|
|
}
|
|
}
|
|
|
|
var actionHtml = row._row_html_action || '-';
|
|
|
|
return [
|
|
html(nameHtml),
|
|
statusText,
|
|
dateCell(row.assigned_at_sort),
|
|
dateCell(row.due_at_sort),
|
|
dateCell(row.completed_at_sort),
|
|
dateCell(row.overdue_at_sort),
|
|
html(escalationHtml),
|
|
dateCell(row.last_activity_sort),
|
|
html(certHtml),
|
|
dateCell(row.valid_until_sort),
|
|
row.cycle_key || '-',
|
|
html(actionHtml)
|
|
];
|
|
});
|
|
}
|
|
|
|
// ── Filter-Logik ─────────────────────────────────────────────
|
|
|
|
var currentStatusFilter = initialStatusFilter;
|
|
var currentSearchTerm = '';
|
|
|
|
function getFilteredRows() {
|
|
var rows = allRows;
|
|
|
|
if (currentStatusFilter) {
|
|
rows = rows.filter(function (row) {
|
|
return row.status === currentStatusFilter;
|
|
});
|
|
}
|
|
|
|
if (currentSearchTerm) {
|
|
var term = currentSearchTerm.toLowerCase();
|
|
rows = rows.filter(function (row) {
|
|
return row._searchText.indexOf(term) !== -1;
|
|
});
|
|
}
|
|
|
|
return rows;
|
|
}
|
|
|
|
function applyFilters() {
|
|
grid.updateConfig({
|
|
data: buildGridData(getFilteredRows())
|
|
}).forceRender();
|
|
}
|
|
|
|
// ── Grid initialisieren ──────────────────────────────────────
|
|
|
|
var grid = new gridjs.Grid({
|
|
columns: [
|
|
{ name: 'Mitarbeiter', width: '180px', sort: { compare: compareHtmlText } },
|
|
{ name: 'Status', width: '130px', sort: { compare: compareStrings } },
|
|
{ name: 'Zugewiesen', width: '130px', formatter: dateCellFormatter, sort: { compare: compareDateValues } },
|
|
{ name: 'Fällig', width: '130px', formatter: dateCellFormatter, sort: { compare: compareDateValues } },
|
|
{ name: 'Abgeschlossen', width: '130px', formatter: dateCellFormatter, sort: { compare: compareDateValues } },
|
|
{ name: 'Überfällig seit', width: '130px', formatter: dateCellFormatter, sort: { compare: compareDateValues } },
|
|
{ name: 'Eskalation', width: '90px', sort: { compare: compareNumeric } },
|
|
{ name: 'Letzte Akt.', width: '130px', formatter: dateCellFormatter, sort: { compare: compareDateValues } },
|
|
{ name: 'Zertifikat', width: '120px', sort: { compare: compareHtmlText } },
|
|
{ name: 'Gültig bis', width: '130px', formatter: dateCellFormatter, sort: { compare: compareDateValues } },
|
|
{ name: 'Zyklus', width: '90px', sort: { compare: compareStrings } },
|
|
{ name: 'Aktion', width: '130px', sort: false }
|
|
],
|
|
data: buildGridData(getFilteredRows()),
|
|
search: false,
|
|
sort: {
|
|
multiColumn: false
|
|
},
|
|
pagination: {
|
|
enabled: true,
|
|
limit: 25
|
|
},
|
|
language: {
|
|
pagination: {
|
|
previous: 'Zurück',
|
|
next: 'Weiter',
|
|
showing: 'Zeige',
|
|
of: 'von',
|
|
to: 'bis',
|
|
results: 'Einträge'
|
|
},
|
|
noRecordsFound: 'Keine Einträge gefunden.',
|
|
loading: 'Laden...',
|
|
error: 'Fehler beim Laden der Daten.'
|
|
},
|
|
className: {
|
|
table: 'task-cert-gridjs-table',
|
|
th: 'task-cert-gridjs-th',
|
|
td: 'task-cert-gridjs-td'
|
|
},
|
|
autoWidth: false
|
|
});
|
|
|
|
grid.render(container);
|
|
|
|
// ── Event-Handler ────────────────────────────────────────────
|
|
|
|
if (statusFilterEl) {
|
|
if (initialStatusFilter) {
|
|
statusFilterEl.value = initialStatusFilter;
|
|
}
|
|
statusFilterEl.addEventListener('change', function () {
|
|
currentStatusFilter = this.value;
|
|
applyFilters();
|
|
});
|
|
}
|
|
|
|
if (searchInputEl) {
|
|
var searchTimeout = null;
|
|
searchInputEl.addEventListener('input', function () {
|
|
var val = this.value.trim();
|
|
if (searchTimeout) { clearTimeout(searchTimeout); }
|
|
searchTimeout = setTimeout(function () {
|
|
currentSearchTerm = val;
|
|
applyFilters();
|
|
}, 200);
|
|
});
|
|
}
|
|
|
|
// CSV-Export Übersicht
|
|
var exportBtn = document.getElementById('task-cert-overview-export');
|
|
if (exportBtn) {
|
|
exportBtn.addEventListener('click', function () {
|
|
var rows = getFilteredRows();
|
|
var headers = ['Mitarbeiter', 'E-Mail', 'Status', 'Zugewiesen', 'Fällig', 'Abgeschlossen',
|
|
'Überfällig seit', 'Eskalation', 'Letzte Aktivität', 'Zertifikat', 'Gültig bis', 'Zyklus'];
|
|
var csvRows = rows.map(function (row) {
|
|
return [
|
|
row.contact_name || '',
|
|
row.contact_email || '',
|
|
row.status_label || '',
|
|
formatDateDisplay(row.assigned_at_sort),
|
|
formatDateDisplay(row.due_at_sort),
|
|
formatDateDisplay(row.completed_at_sort),
|
|
formatDateDisplay(row.overdue_at_sort),
|
|
row.escalation_level != null ? row.escalation_level : '',
|
|
formatDateDisplay(row.last_activity_sort),
|
|
row.certificate_label || '',
|
|
formatDateDisplay(row.valid_until_sort),
|
|
row.cycle_key || ''
|
|
];
|
|
});
|
|
var csv = TaskCertCsvExport.buildCsv(headers, csvRows);
|
|
TaskCertCsvExport.download(csv, 'uebersicht-export.csv');
|
|
});
|
|
}
|
|
})();
|
|
|
|
// ══════════════════════════════════════════════════════════════════
|
|
// Antworten-Grid
|
|
// ══════════════════════════════════════════════════════════════════
|
|
(function () {
|
|
'use strict';
|
|
|
|
var container = document.getElementById('task-cert-answers-grid');
|
|
if (!container) { return; }
|
|
|
|
var dataEl = document.getElementById('task-cert-answers-grid-data');
|
|
if (!dataEl) { return; }
|
|
|
|
var allRows = [];
|
|
try {
|
|
allRows = JSON.parse(dataEl.textContent || '[]');
|
|
} catch (e) {
|
|
container.textContent = 'Fehler beim Laden der Tabellendaten.';
|
|
return;
|
|
}
|
|
|
|
if (!window.gridjs || !window.gridjs.Grid) {
|
|
container.textContent = 'Grid.js konnte nicht geladen werden.';
|
|
return;
|
|
}
|
|
|
|
var html = gridjs.html;
|
|
var searchInputEl = document.getElementById('task-cert-answers-search');
|
|
|
|
function escapeHtml(str) {
|
|
if (!str) { return ''; }
|
|
var div = document.createElement('div');
|
|
div.appendChild(document.createTextNode(String(str)));
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function formatDateDisplay(isoValue) {
|
|
if (!isoValue) { return '-'; }
|
|
try {
|
|
var d = new Date(isoValue);
|
|
if (isNaN(d.getTime())) { return isoValue; }
|
|
var day = ('0' + d.getDate()).slice(-2);
|
|
var month = ('0' + (d.getMonth() + 1)).slice(-2);
|
|
var year = d.getFullYear();
|
|
var hours = ('0' + d.getHours()).slice(-2);
|
|
var minutes = ('0' + d.getMinutes()).slice(-2);
|
|
return day + '.' + month + '.' + year + ' ' + hours + ':' + minutes;
|
|
} catch (e) {
|
|
return isoValue;
|
|
}
|
|
}
|
|
|
|
function compareStrings(a, b) {
|
|
var sa = String(a || '');
|
|
var sb = String(b || '');
|
|
if (sa === '-') { sa = ''; }
|
|
if (sb === '-') { sb = ''; }
|
|
return sa.localeCompare(sb, 'de');
|
|
}
|
|
|
|
function compareDateValues(a, b) {
|
|
var sa = (a && a._sort) || '';
|
|
var sb = (b && b._sort) || '';
|
|
if (sa === sb) { return 0; }
|
|
if (sa === '') { return 1; }
|
|
if (sb === '') { return -1; }
|
|
return sa < sb ? -1 : 1;
|
|
}
|
|
|
|
function compareHtmlText(a, b) {
|
|
var ta = stripHtmlTags(a);
|
|
var tb = stripHtmlTags(b);
|
|
return ta.localeCompare(tb, 'de');
|
|
}
|
|
|
|
function compareNumeric(a, b) {
|
|
var ta = stripHtmlTags(a);
|
|
var tb = stripHtmlTags(b);
|
|
var na = ta === '-' || ta === '' ? -1 : parseFloat(ta) || 0;
|
|
var nb = tb === '-' || tb === '' ? -1 : parseFloat(tb) || 0;
|
|
return na - nb;
|
|
}
|
|
|
|
function stripHtmlTags(val) {
|
|
if (!val) { return ''; }
|
|
if (typeof val === 'object' && val.props && val.props.content) {
|
|
var tmp = document.createElement('div');
|
|
tmp.innerHTML = val.props.content;
|
|
return (tmp.textContent || tmp.innerText || '').trim();
|
|
}
|
|
return String(val);
|
|
}
|
|
|
|
function dateCell(isoValue) {
|
|
return { _sort: isoValue || '', _display: formatDateDisplay(isoValue) };
|
|
}
|
|
|
|
function dateCellFormatter(cell) {
|
|
if (cell && typeof cell === 'object' && cell._display !== undefined) {
|
|
return cell._display;
|
|
}
|
|
return cell || '-';
|
|
}
|
|
|
|
// Suchtext-Cache
|
|
allRows.forEach(function (row) {
|
|
row._searchText = [
|
|
row.contact_name || '',
|
|
row.contact_email || '',
|
|
row.method_label || '',
|
|
row.status_label || '',
|
|
row.score || '',
|
|
formatDateDisplay(row.submitted_at_sort)
|
|
].join(' ').toLowerCase();
|
|
});
|
|
|
|
function buildGridData(rows) {
|
|
return rows.map(function (row) {
|
|
var nameHtml = '<span>' + escapeHtml(row.contact_name || '-') + '</span>';
|
|
if (row.contact_email) {
|
|
nameHtml += '<br><small>' + escapeHtml(row.contact_email) + '</small>';
|
|
}
|
|
|
|
var detailHtml = row.detail_html || '-';
|
|
if (detailHtml !== '-') {
|
|
detailHtml = '<details><summary>Anzeigen</summary>' + detailHtml + '</details>';
|
|
}
|
|
|
|
return [
|
|
html(nameHtml),
|
|
row.method_label || '-',
|
|
row.status_label || '-',
|
|
dateCell(row.submitted_at_sort),
|
|
row.score || '-',
|
|
html(detailHtml)
|
|
];
|
|
});
|
|
}
|
|
|
|
var currentSearchTerm = '';
|
|
|
|
function getFilteredRows() {
|
|
var rows = allRows;
|
|
if (currentSearchTerm) {
|
|
var term = currentSearchTerm.toLowerCase();
|
|
rows = rows.filter(function (row) {
|
|
return row._searchText.indexOf(term) !== -1;
|
|
});
|
|
}
|
|
return rows;
|
|
}
|
|
|
|
function applyFilters() {
|
|
answersGrid.updateConfig({
|
|
data: buildGridData(getFilteredRows())
|
|
}).forceRender();
|
|
}
|
|
|
|
var answersGrid = new gridjs.Grid({
|
|
columns: [
|
|
{ name: 'Mitarbeiter', width: '180px', sort: { compare: compareHtmlText } },
|
|
{ name: 'Methode', width: '130px', sort: { compare: compareStrings } },
|
|
{ name: 'Status', width: '130px', sort: { compare: compareStrings } },
|
|
{ name: 'Eingereicht', width: '140px', formatter: dateCellFormatter, sort: { compare: compareDateValues } },
|
|
{ name: 'Punkte', width: '80px', sort: { compare: compareStrings } },
|
|
{ name: 'Details', width: '250px', sort: false }
|
|
],
|
|
data: buildGridData(getFilteredRows()),
|
|
search: false,
|
|
sort: { multiColumn: false },
|
|
pagination: { enabled: true, limit: 25 },
|
|
language: {
|
|
pagination: {
|
|
previous: 'Zurück',
|
|
next: 'Weiter',
|
|
showing: 'Zeige',
|
|
of: 'von',
|
|
to: 'bis',
|
|
results: 'Einträge'
|
|
},
|
|
noRecordsFound: 'Keine Einträge gefunden.',
|
|
loading: 'Laden...',
|
|
error: 'Fehler beim Laden der Daten.'
|
|
},
|
|
className: {
|
|
table: 'task-cert-gridjs-table',
|
|
th: 'task-cert-gridjs-th',
|
|
td: 'task-cert-gridjs-td'
|
|
},
|
|
autoWidth: false
|
|
});
|
|
|
|
answersGrid.render(container);
|
|
|
|
if (searchInputEl) {
|
|
var searchTimeout = null;
|
|
searchInputEl.addEventListener('input', function () {
|
|
var val = this.value.trim();
|
|
if (searchTimeout) { clearTimeout(searchTimeout); }
|
|
searchTimeout = setTimeout(function () {
|
|
currentSearchTerm = val;
|
|
applyFilters();
|
|
}, 200);
|
|
});
|
|
}
|
|
|
|
// CSV-Export Antworten (mit Antwort-Details als einzelne Spalten)
|
|
var exportBtn = document.getElementById('task-cert-answers-export');
|
|
if (exportBtn) {
|
|
exportBtn.addEventListener('click', function () {
|
|
var rows = getFilteredRows();
|
|
|
|
// Alle einzigartigen Antwort-Labels sammeln (Reihenfolge beibehalten)
|
|
var fieldLabels = [];
|
|
var fieldLabelSet = {};
|
|
for (var i = 0; i < rows.length; i++) {
|
|
var fields = rows[i].answer_fields || {};
|
|
for (var label in fields) {
|
|
if (fields.hasOwnProperty(label) && !fieldLabelSet[label]) {
|
|
fieldLabelSet[label] = true;
|
|
fieldLabels.push(label);
|
|
}
|
|
}
|
|
}
|
|
|
|
var headers = ['Mitarbeiter', 'E-Mail', 'Methode', 'Status', 'Eingereicht', 'Punkte'];
|
|
headers = headers.concat(fieldLabels);
|
|
|
|
var csvRows = rows.map(function (row) {
|
|
var base = [
|
|
row.contact_name || '',
|
|
row.contact_email || '',
|
|
row.method_label || '',
|
|
row.status_label || '',
|
|
formatDateDisplay(row.submitted_at_sort),
|
|
row.score || ''
|
|
];
|
|
var fields = row.answer_fields || {};
|
|
for (var j = 0; j < fieldLabels.length; j++) {
|
|
base.push(fields[fieldLabels[j]] || '');
|
|
}
|
|
return base;
|
|
});
|
|
|
|
var csv = TaskCertCsvExport.buildCsv(headers, csvRows);
|
|
TaskCertCsvExport.download(csv, 'antworten-export.csv');
|
|
});
|
|
}
|
|
})();
|