🎉 Fix for JOBS_UNDEFINED and LOG_EXPORT issues, updated documentation 📚 in backend/docs.
This commit is contained in:
1012
backend/static/js/glassmorphism-notifications.js
Normal file
1012
backend/static/js/glassmorphism-notifications.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -181,7 +181,7 @@ window.refreshStats = async function() {
|
||||
};
|
||||
|
||||
/**
|
||||
* Jobs-Refresh-Funktion
|
||||
* Jobs-Refresh-Funktion - VERBESSERT mit umfassenden Null-Checks
|
||||
*/
|
||||
window.refreshJobs = async function() {
|
||||
const refreshButton = document.getElementById('refresh-button');
|
||||
@@ -194,14 +194,27 @@ window.refreshJobs = async function() {
|
||||
}
|
||||
|
||||
try {
|
||||
// Jobs-Daten neu laden mit verbesserter Fehlerbehandlung
|
||||
if (typeof window.jobManager !== 'undefined' && window.jobManager && window.jobManager.loadJobs) {
|
||||
console.log('🔄 Starte Jobs-Refresh...');
|
||||
|
||||
// Mehrstufige Manager-Prüfung mit erweiterten Sicherheitschecks
|
||||
let refreshSuccess = false;
|
||||
|
||||
if (typeof window.jobManager !== 'undefined' &&
|
||||
window.jobManager &&
|
||||
typeof window.jobManager.loadJobs === 'function') {
|
||||
console.log('📝 Verwende window.jobManager.loadJobs()');
|
||||
await window.jobManager.loadJobs();
|
||||
} else if (typeof jobManager !== 'undefined' && jobManager && jobManager.loadJobs) {
|
||||
refreshSuccess = true;
|
||||
} else if (typeof jobManager !== 'undefined' &&
|
||||
jobManager &&
|
||||
typeof jobManager.loadJobs === 'function') {
|
||||
console.log('📝 Verwende lokalen jobManager.loadJobs()');
|
||||
await jobManager.loadJobs();
|
||||
refreshSuccess = true;
|
||||
} else {
|
||||
// Fallback: API-Aufruf direkt
|
||||
// Fallback: Direkter API-Aufruf mit verbesserter Fehlerbehandlung
|
||||
console.log('📝 JobManager nicht verfügbar - verwende direkten API-Aufruf');
|
||||
|
||||
const response = await fetch('/api/jobs', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -214,28 +227,128 @@ window.refreshJobs = async function() {
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('📝 Jobs erfolgreich über API geladen:', data);
|
||||
console.log('📝 API-Response erhalten:', data);
|
||||
|
||||
// Wenn JobsContainer vorhanden ist, versuche die Jobs zu rendern
|
||||
const jobsContainer = document.querySelector('.jobs-container, #jobs-container, .job-grid');
|
||||
if (jobsContainer && data.jobs) {
|
||||
// Einfache Jobs-Darstellung als Fallback
|
||||
jobsContainer.innerHTML = data.jobs.map(job => `
|
||||
<div class="job-card p-4 border rounded-lg">
|
||||
<h3 class="font-semibold">${job.filename || 'Unbekannter Job'}</h3>
|
||||
<p class="text-sm text-gray-600">Status: ${job.status || 'Unbekannt'}</p>
|
||||
</div>
|
||||
`).join('');
|
||||
// VERBESSERTE Datenvalidierung
|
||||
if (!data || typeof data !== 'object') {
|
||||
throw new Error('Ungültige API-Response: Keine Daten erhalten');
|
||||
}
|
||||
|
||||
// Sichere Jobs-Extraktion
|
||||
let jobs = [];
|
||||
if (Array.isArray(data.jobs)) {
|
||||
jobs = data.jobs;
|
||||
} else if (Array.isArray(data)) {
|
||||
jobs = data;
|
||||
} else if (data.success && Array.isArray(data.data)) {
|
||||
jobs = data.data;
|
||||
} else {
|
||||
console.warn('⚠️ Keine Jobs-Array in API-Response gefunden:', data);
|
||||
jobs = [];
|
||||
}
|
||||
|
||||
console.log(`📝 ${jobs.length} Jobs aus API extrahiert:`, jobs);
|
||||
|
||||
// Container-Updates mit Fallback-Rendering
|
||||
const jobsContainers = [
|
||||
'.jobs-container',
|
||||
'#jobs-container',
|
||||
'.job-grid',
|
||||
'#jobs-list',
|
||||
'#jobs-grid'
|
||||
];
|
||||
|
||||
let containerFound = false;
|
||||
for (const selector of jobsContainers) {
|
||||
const container = document.querySelector(selector);
|
||||
if (container) {
|
||||
containerFound = true;
|
||||
console.log(`📝 Container gefunden: ${selector}`);
|
||||
|
||||
if (jobs.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-12">
|
||||
<div class="text-gray-400 dark:text-gray-600 text-6xl mb-4">📭</div>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">Keine Jobs vorhanden</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400">Es wurden noch keine Druckaufträge erstellt.</p>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
// Sichere Job-Darstellung mit Null-Checks
|
||||
const jobCards = jobs.map(job => {
|
||||
if (!job || typeof job !== 'object') {
|
||||
console.warn('⚠️ Ungültiges Job-Objekt übersprungen:', job);
|
||||
return '';
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="job-card p-4 border rounded-lg bg-white dark:bg-slate-800 shadow-sm hover:shadow-md transition-shadow">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white mb-2">
|
||||
${job.filename || job.title || job.name || 'Unbekannter Job'}
|
||||
</h3>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 space-y-1">
|
||||
<p><span class="font-medium">ID:</span> ${job.id || 'N/A'}</p>
|
||||
<p><span class="font-medium">Status:</span> ${job.status || 'Unbekannt'}</p>
|
||||
${job.printer_name ? `<p><span class="font-medium">Drucker:</span> ${job.printer_name}</p>` : ''}
|
||||
${job.created_at ? `<p><span class="font-medium">Erstellt:</span> ${new Date(job.created_at).toLocaleDateString('de-DE')}</p>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).filter(card => card !== '').join('');
|
||||
|
||||
container.innerHTML = jobCards || `
|
||||
<div class="text-center py-8">
|
||||
<p class="text-gray-500 dark:text-gray-400">Keine gültigen Jobs zum Anzeigen</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!containerFound) {
|
||||
console.warn('⚠️ Kein Jobs-Container gefunden. Verfügbare Container:', jobsContainers);
|
||||
}
|
||||
|
||||
refreshSuccess = true;
|
||||
}
|
||||
|
||||
if (refreshSuccess) {
|
||||
showToast('✅ Druckaufträge erfolgreich aktualisiert', 'success');
|
||||
}
|
||||
|
||||
showToast('✅ Druckaufträge erfolgreich aktualisiert', 'success');
|
||||
} catch (error) {
|
||||
console.error('Jobs-Refresh Fehler:', error);
|
||||
const errorMessage = error.message === 'undefined' ?
|
||||
'Jobs-Manager nicht verfügbar' :
|
||||
error.message || 'Unbekannter Fehler';
|
||||
showToast(`❌ Fehler beim Laden der Jobs: ${errorMessage}`, 'error');
|
||||
console.error('❌ Jobs-Refresh Fehler:', error);
|
||||
|
||||
// Intelligente Fehlermeldung basierend auf dem Fehlertyp
|
||||
let errorMessage;
|
||||
if (error.message.includes('undefined')) {
|
||||
errorMessage = 'Jobs-Daten nicht verfügbar';
|
||||
} else if (error.message.includes('fetch')) {
|
||||
errorMessage = 'Netzwerkfehler beim Laden der Jobs';
|
||||
} else if (error.message.includes('API')) {
|
||||
errorMessage = 'Server-Fehler beim Laden der Jobs';
|
||||
} else {
|
||||
errorMessage = error.message || 'Unbekannter Fehler beim Laden der Jobs';
|
||||
}
|
||||
|
||||
showToast(`❌ Fehler: ${errorMessage}`, 'error');
|
||||
|
||||
// Fallback-Container mit Fehleranzeige
|
||||
const container = document.querySelector('.jobs-container, #jobs-container, .job-grid, #jobs-list');
|
||||
if (container) {
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-12">
|
||||
<div class="text-red-400 dark:text-red-600 text-6xl mb-4">⚠️</div>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">Fehler beim Laden</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400 mb-4">${errorMessage}</p>
|
||||
<button onclick="refreshJobs()" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
|
||||
Erneut versuchen
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
} finally {
|
||||
if (refreshButton) {
|
||||
refreshButton.disabled = false;
|
||||
|
@@ -382,6 +382,12 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// VERBESSERTE SICHERHEITSCHECKS
|
||||
if (!Array.isArray(this.jobs)) {
|
||||
console.warn('⚠️ this.jobs ist kein Array:', this.jobs);
|
||||
this.jobs = [];
|
||||
}
|
||||
|
||||
if (this.jobs.length === 0) {
|
||||
jobsList.innerHTML = `
|
||||
<div class="text-center py-12">
|
||||
@@ -393,10 +399,32 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const jobsHTML = this.jobs.map(job => this.renderJobCard(job)).join('');
|
||||
jobsList.innerHTML = jobsHTML;
|
||||
|
||||
console.log(`📋 ${this.jobs.length} Jobs gerendert`);
|
||||
try {
|
||||
const jobsHTML = this.jobs.map(job => {
|
||||
// Sicherstellen, dass job ein gültiges Objekt ist
|
||||
if (!job || typeof job !== 'object') {
|
||||
console.warn('⚠️ Ungültiges Job-Objekt:', job);
|
||||
return '';
|
||||
}
|
||||
return this.renderJobCard(job);
|
||||
}).filter(html => html !== '').join('');
|
||||
|
||||
jobsList.innerHTML = jobsHTML;
|
||||
|
||||
console.log(`📋 ${this.jobs.length} Jobs gerendert`);
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Rendern der Jobs:', error);
|
||||
jobsList.innerHTML = `
|
||||
<div class="text-center py-12">
|
||||
<div class="text-red-400 dark:text-red-600 text-6xl mb-4">⚠️</div>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">Fehler beim Laden</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400">Es gab einen Fehler beim Darstellen der Jobs.</p>
|
||||
<button onclick="window.jobManager.loadJobs()" class="mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
|
||||
Erneut versuchen
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
307
backend/static/js/jobs-safety-fix.js
Normal file
307
backend/static/js/jobs-safety-fix.js
Normal file
@@ -0,0 +1,307 @@
|
||||
/**
|
||||
* Jobs Safety Fix - Mercedes-Benz MYP Platform
|
||||
* Umfassende Lösung für "jobs undefined" Probleme
|
||||
* Version: 1.0.0
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
console.log('🛡️ Jobs Safety Fix wird geladen...');
|
||||
|
||||
/**
|
||||
* Globale Jobs-Variable sicher initialisieren
|
||||
*/
|
||||
if (typeof window.jobsData === 'undefined') {
|
||||
window.jobsData = [];
|
||||
}
|
||||
|
||||
if (typeof window.filteredJobs === 'undefined') {
|
||||
window.filteredJobs = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sichere Jobs-Funktionen
|
||||
*/
|
||||
window.safeJobsOperations = {
|
||||
/**
|
||||
* Jobs sicher abrufen
|
||||
*/
|
||||
getJobs: function() {
|
||||
if (window.jobManager && Array.isArray(window.jobManager.jobs)) {
|
||||
return window.jobManager.jobs;
|
||||
}
|
||||
if (Array.isArray(window.jobsData)) {
|
||||
return window.jobsData;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Gefilterte Jobs sicher abrufen
|
||||
*/
|
||||
getFilteredJobs: function() {
|
||||
if (Array.isArray(window.filteredJobs)) {
|
||||
return window.filteredJobs;
|
||||
}
|
||||
return this.getJobs();
|
||||
},
|
||||
|
||||
/**
|
||||
* Jobs sicher setzen
|
||||
*/
|
||||
setJobs: function(jobs) {
|
||||
if (!Array.isArray(jobs)) {
|
||||
console.warn('⚠️ Jobs ist kein Array, konvertiere zu leerem Array');
|
||||
jobs = [];
|
||||
}
|
||||
|
||||
if (window.jobManager) {
|
||||
window.jobManager.jobs = jobs;
|
||||
}
|
||||
window.jobsData = jobs;
|
||||
|
||||
console.log(`✅ ${jobs.length} Jobs sicher gesetzt`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Job sicher finden
|
||||
*/
|
||||
findJob: function(jobId) {
|
||||
const jobs = this.getJobs();
|
||||
return jobs.find(job => job && job.id && job.id.toString() === jobId.toString()) || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Jobs sicher filtern
|
||||
*/
|
||||
filterJobs: function(filterFn) {
|
||||
const jobs = this.getJobs();
|
||||
if (typeof filterFn !== 'function') {
|
||||
return jobs;
|
||||
}
|
||||
|
||||
try {
|
||||
return jobs.filter(job => {
|
||||
if (!job || typeof job !== 'object') {
|
||||
return false;
|
||||
}
|
||||
return filterFn(job);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Filtern der Jobs:', error);
|
||||
return jobs;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* JobManager Sicherheitsprüfungen
|
||||
*/
|
||||
function ensureJobManagerSafety() {
|
||||
if (typeof window.jobManager !== 'undefined' && window.jobManager) {
|
||||
// Sicherstellen, dass jobs ein Array ist
|
||||
if (!Array.isArray(window.jobManager.jobs)) {
|
||||
console.warn('⚠️ JobManager.jobs ist kein Array, korrigiere...');
|
||||
window.jobManager.jobs = [];
|
||||
}
|
||||
|
||||
// Originale loadJobs Methode wrappen
|
||||
if (window.jobManager.loadJobs && typeof window.jobManager.loadJobs === 'function') {
|
||||
const originalLoadJobs = window.jobManager.loadJobs;
|
||||
window.jobManager.loadJobs = async function(...args) {
|
||||
try {
|
||||
await originalLoadJobs.apply(this, args);
|
||||
// Nach dem Laden sicherstellen, dass jobs ein Array ist
|
||||
if (!Array.isArray(this.jobs)) {
|
||||
this.jobs = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim sicheren Laden der Jobs:', error);
|
||||
this.jobs = [];
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Originale renderJobs Methode wrappen
|
||||
if (window.jobManager.renderJobs && typeof window.jobManager.renderJobs === 'function') {
|
||||
const originalRenderJobs = window.jobManager.renderJobs;
|
||||
window.jobManager.renderJobs = function(...args) {
|
||||
try {
|
||||
// Sicherstellen, dass jobs ein Array ist vor dem Rendern
|
||||
if (!Array.isArray(this.jobs)) {
|
||||
console.warn('⚠️ Jobs ist kein Array vor dem Rendern, korrigiere...');
|
||||
this.jobs = [];
|
||||
}
|
||||
return originalRenderJobs.apply(this, args);
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim sicheren Rendern der Jobs:', error);
|
||||
// Fallback-Rendering
|
||||
const jobsList = document.getElementById('jobs-list');
|
||||
if (jobsList) {
|
||||
jobsList.innerHTML = `
|
||||
<div class="text-center py-12">
|
||||
<div class="text-red-400 dark:text-red-600 text-6xl mb-4">⚠️</div>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">Renderingfehler</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400 mb-4">Jobs konnten nicht angezeigt werden</p>
|
||||
<button onclick="window.jobManager.loadJobs()" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
|
||||
Erneut laden
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Globale Variablen-Überwachung
|
||||
*/
|
||||
function setupGlobalVariableWatching() {
|
||||
// jobsData überwachen
|
||||
let _jobsData = [];
|
||||
Object.defineProperty(window, 'jobsData', {
|
||||
get: function() {
|
||||
return _jobsData;
|
||||
},
|
||||
set: function(value) {
|
||||
if (!Array.isArray(value)) {
|
||||
console.warn('⚠️ Versuche jobsData mit Non-Array zu setzen:', value);
|
||||
_jobsData = [];
|
||||
} else {
|
||||
_jobsData = value;
|
||||
}
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
|
||||
// filteredJobs überwachen
|
||||
let _filteredJobs = [];
|
||||
Object.defineProperty(window, 'filteredJobs', {
|
||||
get: function() {
|
||||
return _filteredJobs;
|
||||
},
|
||||
set: function(value) {
|
||||
if (!Array.isArray(value)) {
|
||||
console.warn('⚠️ Versuche filteredJobs mit Non-Array zu setzen:', value);
|
||||
_filteredJobs = [];
|
||||
} else {
|
||||
_filteredJobs = value;
|
||||
}
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* API-Response Validator
|
||||
*/
|
||||
window.validateJobsResponse = function(data) {
|
||||
if (!data || typeof data !== 'object') {
|
||||
console.warn('⚠️ Ungültige API-Response:', data);
|
||||
return { jobs: [], total: 0 };
|
||||
}
|
||||
|
||||
let jobs = [];
|
||||
|
||||
// Verschiedene Response-Formate unterstützen
|
||||
if (Array.isArray(data.jobs)) {
|
||||
jobs = data.jobs;
|
||||
} else if (Array.isArray(data.data)) {
|
||||
jobs = data.data;
|
||||
} else if (Array.isArray(data)) {
|
||||
jobs = data;
|
||||
} else if (data.success && Array.isArray(data.jobs)) {
|
||||
jobs = data.jobs;
|
||||
}
|
||||
|
||||
// Jobs validieren
|
||||
jobs = jobs.filter(job => {
|
||||
if (!job || typeof job !== 'object') {
|
||||
console.warn('⚠️ Ungültiges Job-Objekt gefiltert:', job);
|
||||
return false;
|
||||
}
|
||||
if (!job.id) {
|
||||
console.warn('⚠️ Job ohne ID gefiltert:', job);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
console.log(`✅ ${jobs.length} Jobs validiert`);
|
||||
return {
|
||||
jobs: jobs,
|
||||
total: jobs.length,
|
||||
current_page: data.current_page || 1,
|
||||
total_pages: data.total_pages || 1
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Fehlerbehandlung für undefined Jobs
|
||||
*/
|
||||
window.addEventListener('error', function(e) {
|
||||
if (e.message && e.message.includes('jobs') && e.message.includes('undefined')) {
|
||||
console.log('🛡️ Jobs undefined Fehler abgefangen:', e.message);
|
||||
|
||||
// Versuche Jobs zu reparieren
|
||||
window.safeJobsOperations.setJobs([]);
|
||||
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Promise rejection handler für Jobs
|
||||
*/
|
||||
window.addEventListener('unhandledrejection', function(e) {
|
||||
if (e.reason && e.reason.message && e.reason.message.includes('jobs')) {
|
||||
console.log('🛡️ Jobs Promise rejection abgefangen:', e.reason);
|
||||
|
||||
// Versuche Jobs zu reparieren
|
||||
window.safeJobsOperations.setJobs([]);
|
||||
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DOM bereit - Setup starten
|
||||
*/
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
setupGlobalVariableWatching();
|
||||
ensureJobManagerSafety();
|
||||
});
|
||||
} else {
|
||||
setupGlobalVariableWatching();
|
||||
ensureJobManagerSafety();
|
||||
}
|
||||
|
||||
/**
|
||||
* Periodische Sicherheitsprüfung
|
||||
*/
|
||||
setInterval(function() {
|
||||
ensureJobManagerSafety();
|
||||
|
||||
// Prüfe ob globale Jobs-Variablen noch Arrays sind
|
||||
if (!Array.isArray(window.jobsData)) {
|
||||
console.warn('⚠️ jobsData ist kein Array mehr, repariere...');
|
||||
window.jobsData = [];
|
||||
}
|
||||
|
||||
if (!Array.isArray(window.filteredJobs)) {
|
||||
console.warn('⚠️ filteredJobs ist kein Array mehr, repariere...');
|
||||
window.filteredJobs = [];
|
||||
}
|
||||
}, 10000); // Alle 10 Sekunden
|
||||
|
||||
console.log('✅ Jobs Safety Fix erfolgreich geladen');
|
||||
|
||||
})();
|
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* Benachrichtigungssystem für die MYP 3D-Druck Platform
|
||||
* Verwaltet die Anzeige und Interaktion mit Benachrichtigungen
|
||||
* Modernes Benachrichtigungssystem für die MYP 3D-Druck Platform
|
||||
* Verwaltet einheitliche Glassmorphism-Benachrichtigungen aller Art
|
||||
*/
|
||||
|
||||
class NotificationManager {
|
||||
class ModernNotificationManager {
|
||||
constructor() {
|
||||
this.notificationToggle = document.getElementById('notificationToggle');
|
||||
this.notificationDropdown = document.getElementById('notificationDropdown');
|
||||
@@ -13,11 +13,14 @@ class NotificationManager {
|
||||
|
||||
this.isOpen = false;
|
||||
this.notifications = [];
|
||||
this.activeToasts = new Map();
|
||||
this.toastCounter = 0;
|
||||
|
||||
// CSRF-Token aus Meta-Tag holen
|
||||
this.csrfToken = this.getCSRFToken();
|
||||
|
||||
this.init();
|
||||
this.setupGlobalNotificationSystem();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -307,9 +310,358 @@ class NotificationManager {
|
||||
console.error('Fehler beim Markieren aller als gelesen:', error);
|
||||
}
|
||||
}
|
||||
|
||||
setupGlobalNotificationSystem() {
|
||||
// Globale Funktionen für einheitliche Benachrichtigungen
|
||||
window.showFlashMessage = this.showGlassToast.bind(this);
|
||||
window.showToast = this.showGlassToast.bind(this);
|
||||
window.showNotification = this.showGlassToast.bind(this);
|
||||
window.showSuccessMessage = (message) => this.showGlassToast(message, 'success');
|
||||
window.showErrorMessage = (message) => this.showGlassToast(message, 'error');
|
||||
window.showWarningMessage = (message) => this.showGlassToast(message, 'warning');
|
||||
window.showInfoMessage = (message) => this.showGlassToast(message, 'info');
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt eine moderne Glassmorphism-Toast-Benachrichtigung
|
||||
*/
|
||||
showGlassToast(message, type = 'info', duration = 5000, options = {}) {
|
||||
// DND-Check
|
||||
if (window.dndManager && window.dndManager.isEnabled) {
|
||||
window.dndManager.suppressNotification(message, type);
|
||||
return;
|
||||
}
|
||||
|
||||
const toastId = `toast-${++this.toastCounter}`;
|
||||
|
||||
// Toast-Element erstellen
|
||||
const toast = document.createElement('div');
|
||||
toast.id = toastId;
|
||||
toast.className = `glass-toast notification notification-${type} show`;
|
||||
|
||||
// Icon basierend auf Typ
|
||||
const iconMap = {
|
||||
success: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>`,
|
||||
error: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>`,
|
||||
warning: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"/>
|
||||
</svg>`,
|
||||
info: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>`
|
||||
};
|
||||
|
||||
toast.innerHTML = `
|
||||
<div class="flex items-center">
|
||||
<div class="notification-icon">
|
||||
${iconMap[type] || iconMap.info}
|
||||
</div>
|
||||
<div class="notification-content">
|
||||
${options.title ? `<div class="notification-title">${options.title}</div>` : ''}
|
||||
<div class="notification-message">${message}</div>
|
||||
</div>
|
||||
<button class="notification-close" onclick="modernNotificationManager.closeToast('${toastId}')">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Container für Toasts erstellen falls nicht vorhanden
|
||||
let container = document.getElementById('toast-container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.id = 'toast-container';
|
||||
container.className = 'notifications-container';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
// Position berechnen basierend auf existierenden Toasts
|
||||
const existingToasts = container.children.length;
|
||||
toast.style.top = `${1 + existingToasts * 5}rem`;
|
||||
|
||||
container.appendChild(toast);
|
||||
this.activeToasts.set(toastId, toast);
|
||||
|
||||
// Hover-Effekt pausiert Auto-Close
|
||||
let isPaused = false;
|
||||
let timeoutId;
|
||||
|
||||
const startTimer = () => {
|
||||
if (!isPaused && duration > 0) {
|
||||
timeoutId = setTimeout(() => {
|
||||
this.closeToast(toastId);
|
||||
}, duration);
|
||||
}
|
||||
};
|
||||
|
||||
toast.addEventListener('mouseenter', () => {
|
||||
isPaused = true;
|
||||
clearTimeout(timeoutId);
|
||||
toast.style.transform = 'translateY(-2px) scale(1.02)';
|
||||
});
|
||||
|
||||
toast.addEventListener('mouseleave', () => {
|
||||
isPaused = false;
|
||||
toast.style.transform = 'translateY(0) scale(1)';
|
||||
startTimer();
|
||||
});
|
||||
|
||||
// Initial timer starten
|
||||
setTimeout(startTimer, 100);
|
||||
|
||||
// Sound-Effekt (optional)
|
||||
if (options.playSound !== false) {
|
||||
this.playNotificationSound(type);
|
||||
}
|
||||
|
||||
return toastId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schließt eine spezifische Toast-Benachrichtigung
|
||||
*/
|
||||
closeToast(toastId) {
|
||||
const toast = this.activeToasts.get(toastId);
|
||||
if (!toast) return;
|
||||
|
||||
toast.classList.add('hiding');
|
||||
|
||||
setTimeout(() => {
|
||||
if (toast.parentNode) {
|
||||
toast.parentNode.removeChild(toast);
|
||||
}
|
||||
this.activeToasts.delete(toastId);
|
||||
this.repositionToasts();
|
||||
}, 400);
|
||||
}
|
||||
|
||||
/**
|
||||
* Repositioniert alle aktiven Toasts
|
||||
*/
|
||||
repositionToasts() {
|
||||
let index = 0;
|
||||
this.activeToasts.forEach((toast) => {
|
||||
if (toast.parentNode) {
|
||||
toast.style.top = `${1 + index * 5}rem`;
|
||||
index++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Spielt einen Benachrichtigungston ab
|
||||
*/
|
||||
playNotificationSound(type) {
|
||||
try {
|
||||
// Nur wenn AudioContext verfügbar ist
|
||||
if (typeof AudioContext !== 'undefined' || typeof webkitAudioContext !== 'undefined') {
|
||||
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
const oscillator = audioContext.createOscillator();
|
||||
const gainNode = audioContext.createGain();
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(audioContext.destination);
|
||||
|
||||
// Verschiedene Töne für verschiedene Typen
|
||||
const frequencies = {
|
||||
success: [523.25, 659.25, 783.99], // C-E-G Akkord
|
||||
error: [440, 415.3], // A-Ab
|
||||
warning: [493.88, 523.25], // B-C
|
||||
info: [523.25] // C
|
||||
};
|
||||
|
||||
const freq = frequencies[type] || frequencies.info;
|
||||
|
||||
freq.forEach((f, i) => {
|
||||
setTimeout(() => {
|
||||
const osc = audioContext.createOscillator();
|
||||
const gain = audioContext.createGain();
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(audioContext.destination);
|
||||
|
||||
osc.frequency.setValueAtTime(f, audioContext.currentTime);
|
||||
gain.gain.setValueAtTime(0.1, audioContext.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1);
|
||||
|
||||
osc.start(audioContext.currentTime);
|
||||
osc.stop(audioContext.currentTime + 0.1);
|
||||
}, i * 100);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// Silent fail - Audio nicht verfügbar
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt Browser-Benachrichtigung (falls berechtigt)
|
||||
*/
|
||||
showBrowserNotification(title, message, options = {}) {
|
||||
if ('Notification' in window) {
|
||||
if (Notification.permission === 'granted') {
|
||||
const notification = new Notification(title, {
|
||||
body: message,
|
||||
icon: options.icon || '/static/icons/static/icons/notification-icon.png',
|
||||
badge: options.badge || '/static/icons/static/icons/badge-icon.png',
|
||||
tag: options.tag || 'myp-notification',
|
||||
requireInteraction: options.requireInteraction || false,
|
||||
...options
|
||||
});
|
||||
|
||||
notification.onclick = options.onClick || (() => {
|
||||
window.focus();
|
||||
notification.close();
|
||||
});
|
||||
|
||||
return notification;
|
||||
} else if (Notification.permission === 'default') {
|
||||
Notification.requestPermission().then(permission => {
|
||||
if (permission === 'granted') {
|
||||
this.showBrowserNotification(title, message, options);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt eine Alert-Benachrichtigung mit Glassmorphism
|
||||
*/
|
||||
showAlert(message, type = 'info', options = {}) {
|
||||
const alertId = `alert-${Date.now()}`;
|
||||
const alert = document.createElement('div');
|
||||
alert.id = alertId;
|
||||
alert.className = `alert alert-${type}`;
|
||||
|
||||
alert.innerHTML = `
|
||||
<div class="flex items-start">
|
||||
<div class="notification-icon mr-3">
|
||||
${this.getIconForType(type)}
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
${options.title ? `<h4 class="font-semibold mb-2">${options.title}</h4>` : ''}
|
||||
<p>${message}</p>
|
||||
</div>
|
||||
${options.dismissible !== false ? `
|
||||
<button class="ml-3 p-1 rounded-lg opacity-70 hover:opacity-100 transition-opacity" onclick="document.getElementById('${alertId}').remove()">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Alert in Container einfügen
|
||||
const container = options.container || document.querySelector('.flash-messages') || document.body;
|
||||
container.appendChild(alert);
|
||||
|
||||
// Auto-dismiss
|
||||
if (options.autoDismiss !== false) {
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.style.opacity = '0';
|
||||
alert.style.transform = 'translateY(-20px)';
|
||||
setTimeout(() => alert.remove(), 300);
|
||||
}
|
||||
}, options.duration || 7000);
|
||||
}
|
||||
|
||||
return alertId;
|
||||
}
|
||||
|
||||
getIconForType(type) {
|
||||
const icons = {
|
||||
success: `<svg class="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>`,
|
||||
error: `<svg class="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>`,
|
||||
warning: `<svg class="w-5 h-5 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"/>
|
||||
</svg>`,
|
||||
info: `<svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>`
|
||||
};
|
||||
return icons[type] || icons.info;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialisierung nach DOM-Load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new NotificationManager();
|
||||
});
|
||||
// Legacy NotificationManager für Rückwärtskompatibilität
|
||||
class NotificationManager extends ModernNotificationManager {
|
||||
constructor() {
|
||||
super();
|
||||
console.warn('NotificationManager ist deprecated. Verwenden Sie ModernNotificationManager.');
|
||||
}
|
||||
}
|
||||
|
||||
// Globale Instanz erstellen
|
||||
const modernNotificationManager = new ModernNotificationManager();
|
||||
|
||||
// Für Rückwärtskompatibilität
|
||||
if (typeof window !== 'undefined') {
|
||||
window.notificationManager = modernNotificationManager;
|
||||
window.modernNotificationManager = modernNotificationManager;
|
||||
}
|
||||
|
||||
// CSS für glassmorphe Toasts hinzufügen
|
||||
const toastStyles = `
|
||||
<style>
|
||||
.glass-toast {
|
||||
position: relative;
|
||||
transform: translateX(0) translateY(0) scale(1);
|
||||
animation: toast-slide-in 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.glass-toast:hover {
|
||||
transform: translateY(-2px) scale(1.02) !important;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes toast-slide-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(100%) translateY(-20px) scale(0.9);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
transform: translateX(20px) translateY(-10px) scale(1.05);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateX(0) translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.notification-item.unread {
|
||||
background: linear-gradient(135deg,
|
||||
rgba(59, 130, 246, 0.08) 0%,
|
||||
rgba(147, 197, 253, 0.05) 100%);
|
||||
border-left: 3px solid rgb(59, 130, 246);
|
||||
}
|
||||
|
||||
.dark .notification-item.unread {
|
||||
background: linear-gradient(135deg,
|
||||
rgba(59, 130, 246, 0.15) 0%,
|
||||
rgba(147, 197, 253, 0.08) 100%);
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
|
||||
// Styles zur Seite hinzufügen
|
||||
if (typeof document !== 'undefined' && !document.getElementById('toast-styles')) {
|
||||
const styleElement = document.createElement('div');
|
||||
styleElement.id = 'toast-styles';
|
||||
styleElement.innerHTML = toastStyles;
|
||||
document.head.appendChild(styleElement);
|
||||
}
|
Reference in New Issue
Block a user