2025-06-04 10:03:22 +02:00

778 lines
30 KiB
JavaScript

/**
* MYP Platform Job Manager
* Verwaltung und Steuerung von 3D-Druckaufträgen
* Version: 1.0.0
*/
(function() {
'use strict';
/**
* Job Manager Klasse für 3D-Druckaufträge
*/
class JobManager {
constructor() {
this.jobs = [];
this.currentPage = 1;
this.totalPages = 1;
this.isLoading = false;
this.refreshInterval = null;
this.autoRefreshEnabled = false;
console.log('🔧 JobManager wird initialisiert...');
}
/**
* JobManager initialisieren
*/
async init() {
try {
console.log('🚀 JobManager-Initialisierung gestartet');
// Event-Listener einrichten
this.setupEventListeners();
// Formular-Handler einrichten
this.setupFormHandlers();
// Anfängliche Jobs laden
await this.loadJobs();
// Auto-Refresh starten falls aktiviert
if (this.autoRefreshEnabled) {
this.startAutoRefresh();
}
console.log('✅ JobManager erfolgreich initialisiert');
} catch (error) {
console.error('❌ Fehler bei JobManager-Initialisierung:', error);
this.showToast('Fehler beim Initialisieren des Job-Managers', 'error');
}
}
/**
* Event-Listener für Job-Aktionen einrichten
*/
setupEventListeners() {
console.log('📡 Event-Listener werden eingerichtet...');
// Job-Aktionen über data-Attribute
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-job-action]');
if (!target) return;
const action = target.getAttribute('data-job-action');
const jobId = target.getAttribute('data-job-id');
if (!jobId) {
console.warn('⚠️ Job-ID fehlt für Aktion:', action);
return;
}
switch (action) {
case 'start':
this.startJob(jobId);
break;
case 'pause':
this.pauseJob(jobId);
break;
case 'resume':
this.resumeJob(jobId);
break;
case 'stop':
this.stopJob(jobId);
break;
case 'delete':
this.deleteJob(jobId);
break;
case 'details':
this.openJobDetails(jobId);
break;
default:
console.warn('⚠️ Unbekannte Job-Aktion:', action);
}
});
// Refresh-Button
const refreshBtn = document.getElementById('refresh-jobs');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => this.loadJobs());
}
// Auto-Refresh Toggle
const autoRefreshToggle = document.getElementById('auto-refresh-toggle');
if (autoRefreshToggle) {
autoRefreshToggle.addEventListener('change', (e) => {
this.autoRefreshEnabled = e.target.checked;
if (this.autoRefreshEnabled) {
this.startAutoRefresh();
} else {
this.stopAutoRefresh();
}
});
}
console.log('✅ Event-Listener erfolgreich eingerichtet');
}
/**
* Formular-Handler für Job-Erstellung einrichten
*/
setupFormHandlers() {
console.log('📝 Formular-Handler werden eingerichtet...');
const newJobForm = document.getElementById('new-job-form');
if (newJobForm) {
newJobForm.addEventListener('submit', async (e) => {
e.preventDefault();
await this.createNewJob(new FormData(newJobForm));
});
}
const editJobForm = document.getElementById('edit-job-form');
if (editJobForm) {
editJobForm.addEventListener('submit', async (e) => {
e.preventDefault();
const jobId = editJobForm.getAttribute('data-job-id');
await this.updateJob(jobId, new FormData(editJobForm));
});
}
console.log('✅ Formular-Handler erfolgreich eingerichtet');
}
/**
* Jobs von Server laden
*/
async loadJobs(page = 1) {
if (this.isLoading) {
console.log('⚠️ Jobs werden bereits geladen...');
return;
}
this.isLoading = true;
this.showLoadingState(true);
try {
console.log(`📥 Lade Jobs (Seite ${page})...`);
const response = await fetch(`/api/jobs?page=${page}`, {
headers: {
'X-CSRFToken': this.getCSRFToken(),
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
// VERBESSERTE NULL-CHECKS für jobs-Daten
if (data && typeof data === 'object') {
// Sicherstellen, dass jobs immer ein Array ist
this.jobs = Array.isArray(data.jobs) ? data.jobs : [];
this.currentPage = Number(data.current_page) || 1;
this.totalPages = Number(data.total_pages) || 1;
console.log(`${this.jobs.length} Jobs erfolgreich geladen`, this.jobs);
} else {
console.warn('⚠️ Unerwartete API-Response-Struktur:', data);
this.jobs = [];
this.currentPage = 1;
this.totalPages = 1;
}
// Sichere Rendering-Aufrufe
this.renderJobs();
this.updatePagination();
console.log(`${this.jobs.length} Jobs erfolgreich geladen und gerendert`);
} catch (error) {
console.error('❌ Fehler beim Laden der Jobs:', error);
this.showToast('Fehler beim Laden der Jobs', 'error');
// Fallback: Leere Jobs-Liste anzeigen
this.jobs = [];
this.currentPage = 1;
this.totalPages = 1;
this.renderJobs();
} finally {
this.isLoading = false;
this.showLoadingState(false);
}
}
/**
* Job starten
*/
async startJob(jobId) {
try {
console.log(`▶️ Starte Job ${jobId}...`);
const response = await fetch(`/api/jobs/${jobId}/start`, {
method: 'POST',
headers: {
'X-CSRFToken': this.getCSRFToken(),
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
this.showToast('Job erfolgreich gestartet', 'success');
await this.loadJobs();
} catch (error) {
console.error('❌ Fehler beim Starten des Jobs:', error);
this.showToast('Fehler beim Starten des Jobs', 'error');
}
}
/**
* Job pausieren
*/
async pauseJob(jobId) {
try {
console.log(`⏸️ Pausiere Job ${jobId}...`);
const response = await fetch(`/api/jobs/${jobId}/pause`, {
method: 'POST',
headers: {
'X-CSRFToken': this.getCSRFToken(),
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
this.showToast('Job erfolgreich pausiert', 'success');
await this.loadJobs();
} catch (error) {
console.error('❌ Fehler beim Pausieren des Jobs:', error);
this.showToast('Fehler beim Pausieren des Jobs', 'error');
}
}
/**
* Job fortsetzen
*/
async resumeJob(jobId) {
try {
console.log(`▶️ Setze Job ${jobId} fort...`);
const response = await fetch(`/api/jobs/${jobId}/resume`, {
method: 'POST',
headers: {
'X-CSRFToken': this.getCSRFToken(),
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
this.showToast('Job erfolgreich fortgesetzt', 'success');
await this.loadJobs();
} catch (error) {
console.error('❌ Fehler beim Fortsetzen des Jobs:', error);
this.showToast('Fehler beim Fortsetzen des Jobs', 'error');
}
}
/**
* Job stoppen
*/
async stopJob(jobId) {
if (!confirm('Möchten Sie diesen Job wirklich stoppen?')) {
return;
}
try {
console.log(`⏹️ Stoppe Job ${jobId}...`);
const response = await fetch(`/api/jobs/${jobId}/stop`, {
method: 'POST',
headers: {
'X-CSRFToken': this.getCSRFToken(),
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
this.showToast('Job erfolgreich gestoppt', 'success');
await this.loadJobs();
} catch (error) {
console.error('❌ Fehler beim Stoppen des Jobs:', error);
this.showToast('Fehler beim Stoppen des Jobs', 'error');
}
}
/**
* Job löschen
*/
async deleteJob(jobId) {
if (!confirm('Möchten Sie diesen Job wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.')) {
return;
}
try {
console.log(`🗑️ Lösche Job ${jobId}...`);
const response = await fetch(`/api/jobs/${jobId}`, {
method: 'DELETE',
headers: {
'X-CSRFToken': this.getCSRFToken(),
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
this.showToast('Job erfolgreich gelöscht', 'success');
await this.loadJobs();
} catch (error) {
console.error('❌ Fehler beim Löschen des Jobs:', error);
this.showToast('Fehler beim Löschen des Jobs', 'error');
}
}
/**
* Job-Details öffnen
*/
openJobDetails(jobId) {
console.log(`📄 Öffne Details für Job ${jobId}...`);
// Modal für Job-Details öffnen oder zu Detail-Seite navigieren
const detailsUrl = `/jobs/${jobId}`;
// Prüfen ob Modal verfügbar ist
const detailsModal = document.getElementById(`job-details-${jobId}`);
if (detailsModal && typeof window.MYP !== 'undefined' && window.MYP.UI && window.MYP.UI.modal) {
window.MYP.UI.modal.open(`job-details-${jobId}`);
} else {
// Fallback: Zur Detail-Seite navigieren
window.location.href = detailsUrl;
}
}
/**
* Jobs in der UI rendern
*/
renderJobs() {
const jobsList = document.getElementById('jobs-list');
if (!jobsList) {
console.warn('⚠️ Jobs-Liste Element nicht gefunden');
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">
<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>
`;
return;
}
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>
`;
}
}
/**
* Einzelne Job-Karte rendern
*/
renderJobCard(job) {
const statusClass = this.getJobStatusClass(job.status);
const statusText = this.getJobStatusText(job.status);
return `
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<div class="flex items-start justify-between">
<div class="flex-1">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">${job.name || 'Unbenannter Job'}</h3>
<div class="flex items-center space-x-4 text-sm text-gray-500 dark:text-gray-400 mb-4">
<span>ID: ${job.id}</span>
<span>•</span>
<span>Drucker: ${job.printer_name || 'Unbekannt'}</span>
<span>•</span>
<span>Erstellt: ${new Date(job.created_at).toLocaleDateString('de-DE')}</span>
</div>
<div class="flex items-center space-x-2 mb-4">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${statusClass}">
${statusText}
</span>
${job.progress ? `<span class="text-sm text-gray-500 dark:text-gray-400">${job.progress}%</span>` : ''}
</div>
</div>
<div class="flex flex-col space-y-2 ml-4">
${this.renderJobActions(job)}
</div>
</div>
</div>
`;
}
/**
* Job-Aktionen rendern
*/
renderJobActions(job) {
const actions = [];
// Details-Button immer verfügbar
actions.push(`
<button data-job-action="details" data-job-id="${job.id}"
class="inline-flex items-center px-3 py-2 border border-gray-300 dark:border-gray-600 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Details
</button>
`);
// Status-abhängige Aktionen
switch (job.status) {
case 'pending':
case 'ready':
actions.push(`
<button data-job-action="start" data-job-id="${job.id}"
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
Starten
</button>
`);
break;
case 'running':
case 'printing':
actions.push(`
<button data-job-action="pause" data-job-id="${job.id}"
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-yellow-600 hover:bg-yellow-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-yellow-500">
Pausieren
</button>
`);
actions.push(`
<button data-job-action="stop" data-job-id="${job.id}"
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
Stoppen
</button>
`);
break;
case 'paused':
actions.push(`
<button data-job-action="resume" data-job-id="${job.id}"
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
Fortsetzen
</button>
`);
actions.push(`
<button data-job-action="stop" data-job-id="${job.id}"
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
Stoppen
</button>
`);
break;
case 'completed':
case 'failed':
case 'cancelled':
actions.push(`
<button data-job-action="delete" data-job-id="${job.id}"
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
Löschen
</button>
`);
break;
}
return actions.join('');
}
/**
* CSS-Klasse für Job-Status
*/
getJobStatusClass(status) {
const statusClasses = {
'pending': 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300',
'ready': 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300',
'running': 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300',
'printing': 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300',
'paused': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300',
'completed': 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-300',
'failed': 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300',
'cancelled': 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'
};
return statusClasses[status] || statusClasses['pending'];
}
/**
* Anzeigetext für Job-Status
*/
getJobStatusText(status) {
const statusTexts = {
'pending': 'Wartend',
'ready': 'Bereit',
'running': 'Läuft',
'printing': 'Druckt',
'paused': 'Pausiert',
'completed': 'Abgeschlossen',
'failed': 'Fehlgeschlagen',
'cancelled': 'Abgebrochen'
};
return statusTexts[status] || 'Unbekannt';
}
/**
* Loading-Zustand anzeigen/verstecken
*/
showLoadingState(show) {
const loadingEl = document.getElementById('jobs-loading');
const jobsList = document.getElementById('jobs-list');
if (loadingEl) {
loadingEl.style.display = show ? 'block' : 'none';
}
if (jobsList) {
jobsList.style.opacity = show ? '0.5' : '1';
jobsList.style.pointerEvents = show ? 'none' : 'auto';
}
}
/**
* CSRF-Token abrufen
*/
getCSRFToken() {
const token = document.querySelector('meta[name="csrf-token"]');
return token ? token.getAttribute('content') : '';
}
/**
* Toast-Nachricht anzeigen
*/
showToast(message, type = 'info') {
if (typeof window.showToast === 'function') {
window.showToast(message, type);
} else {
console.log(`${type.toUpperCase()}: ${message}`);
}
}
/**
* Auto-Refresh starten
*/
startAutoRefresh() {
this.stopAutoRefresh(); // Vorherigen Refresh stoppen
this.refreshInterval = setInterval(() => {
if (!this.isLoading) {
this.loadJobs(this.currentPage);
}
}, 30000); // Alle 30 Sekunden
console.log('🔄 Auto-Refresh gestartet (30s Intervall)');
}
/**
* Auto-Refresh stoppen
*/
stopAutoRefresh() {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
this.refreshInterval = null;
console.log('⏹️ Auto-Refresh gestoppt');
}
}
/**
* Paginierung aktualisieren
*/
updatePagination() {
const paginationEl = document.getElementById('jobs-pagination');
if (!paginationEl) return;
if (this.totalPages <= 1) {
paginationEl.style.display = 'none';
return;
}
paginationEl.style.display = 'flex';
let paginationHTML = '';
// Vorherige Seite
if (this.currentPage > 1) {
paginationHTML += `
<button onclick="jobManager.loadJobs(${this.currentPage - 1})"
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-l-md hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700">
Zurück
</button>
`;
}
// Seitenzahlen
for (let i = 1; i <= this.totalPages; i++) {
const isActive = i === this.currentPage;
paginationHTML += `
<button onclick="jobManager.loadJobs(${i})"
class="px-3 py-2 text-sm font-medium ${isActive
? 'text-blue-600 bg-blue-50 border-blue-300 dark:bg-blue-900 dark:text-blue-300'
: 'text-gray-500 bg-white border-gray-300 hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700'
} border">
${i}
</button>
`;
}
// Nächste Seite
if (this.currentPage < this.totalPages) {
paginationHTML += `
<button onclick="jobManager.loadJobs(${this.currentPage + 1})"
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-r-md hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700">
Weiter
</button>
`;
}
paginationEl.innerHTML = paginationHTML;
}
/**
* Neuen Job erstellen
*/
async createNewJob(formData) {
try {
console.log('📝 Erstelle neuen Job...');
const response = await fetch('/api/jobs', {
method: 'POST',
headers: {
'X-CSRFToken': this.getCSRFToken()
},
body: formData
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
this.showToast('Job erfolgreich erstellt', 'success');
// Jobs neu laden
await this.loadJobs();
// Formular zurücksetzen
const form = document.getElementById('new-job-form');
if (form) {
form.reset();
}
return result;
} catch (error) {
console.error('❌ Fehler beim Erstellen des Jobs:', error);
this.showToast('Fehler beim Erstellen des Jobs', 'error');
throw error;
}
}
/**
* Job aktualisieren
*/
async updateJob(jobId, formData) {
try {
console.log(`📝 Aktualisiere Job ${jobId}...`);
const response = await fetch(`/api/jobs/${jobId}`, {
method: 'PUT',
headers: {
'X-CSRFToken': this.getCSRFToken()
},
body: formData
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
this.showToast('Job erfolgreich aktualisiert', 'success');
// Jobs neu laden
await this.loadJobs();
return result;
} catch (error) {
console.error('❌ Fehler beim Aktualisieren des Jobs:', error);
this.showToast('Fehler beim Aktualisieren des Jobs', 'error');
throw error;
}
}
}
// JobManager global verfügbar machen
window.JobManager = JobManager;
// JobManager-Instanz erstellen wenn DOM bereit ist
document.addEventListener('DOMContentLoaded', function() {
if (typeof window.jobManager === 'undefined') {
window.jobManager = new JobManager();
// Nur initialisieren wenn wir uns auf einer Jobs-Seite befinden
if (document.getElementById('jobs-list') || document.querySelector('[data-job-action]')) {
window.jobManager.init();
}
}
});
console.log('✅ JobManager-Modul geladen');
})();