1315 lines
60 KiB
HTML

{% extends "base.html" %}
{% block title %}Jobs - MYP Platform{% endblock %}
{% block content %}
<div class="space-y-8">
<!-- Header mit Bereichstitel -->
<div class="relative overflow-hidden rounded-2xl p-6 bg-white/70 dark:bg-slate-800/70 backdrop-blur-lg border border-gray-200/80 dark:border-slate-700/30 shadow-xl transition-all duration-300">
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between">
<div>
<h1 class="text-3xl font-bold text-slate-900 dark:text-white">Job-Reservierungen</h1>
<p class="mt-2 text-slate-500 dark:text-slate-400">Verwalten Sie 3D-Druckjobs und Druckerreservierungen</p>
</div>
<div class="flex mt-4 lg:mt-0">
<button onclick="refreshJobs()" class="bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 text-white px-6 py-2.5 rounded-xl transition-colors duration-300">
<svg class="h-5 w-5 mr-2 inline" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Aktualisieren
</button>
</div>
</div>
</div>
<!-- Neue Reservierung anlegen -->
<div class="relative overflow-hidden rounded-2xl p-6 bg-white/70 dark:bg-slate-800/70 backdrop-blur-lg border border-gray-200/80 dark:border-slate-700/30 shadow-xl transition-all duration-300">
<h2 class="text-xl font-semibold mb-6 text-slate-900 dark:text-white">Neue Reservierung anlegen</h2>
<form id="newJobForm" class="space-y-6">
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
<!-- Drucker auswählen -->
<div>
<label for="printer_id" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Drucker auswählen*</label>
<select id="printer_id" name="printer_id" required class="w-full px-4 py-3 rounded-xl border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700/50 text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 shadow-sm transition-colors duration-200">
<option value="">Drucker auswählen...</option>
<!-- Wird durch JavaScript gefüllt -->
</select>
<div id="printer-status-info" class="mt-2 text-center">
<!-- Status-Info wird dynamisch gefüllt -->
</div>
<div id="printer-status-warning" class="mt-2 hidden">
<div class="flex items-center p-4 bg-red-50 dark:bg-red-900/20 border-2 border-red-400 dark:border-red-700 rounded-lg shadow-sm">
<div class="flex-shrink-0 w-10 h-10 mr-3 flex items-center justify-center rounded-full bg-red-100 dark:bg-red-800">
<svg class="w-6 h-6 text-red-600 dark:text-red-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<div>
<p class="text-lg font-bold text-red-800 dark:text-red-200">ACHTUNG: Offline-Drucker ausgewählt!</p>
<p class="text-sm text-red-700 dark:text-red-300 mt-1">
Dieser Drucker ist derzeit <span class="font-bold">NICHT VERFÜGBAR</span>.
Der Job wird in der Warteschlange gespeichert und erst gestartet,
wenn der Drucker wieder online ist.
</p>
</div>
</div>
</div>
</div>
<!-- Gewünschte Startzeit -->
<div>
<label for="start_time" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Gewünschte Startzeit*</label>
<input type="datetime-local" id="start_time" name="start_time" required
class="w-full px-4 py-3 rounded-xl border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700/50 text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 shadow-sm transition-colors duration-200">
</div>
<!-- Geschätzte Druckdauer -->
<div>
<label for="duration" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Geschätzte Druckdauer (Minuten)*</label>
<input type="number" id="duration" name="duration" required min="1" max="7200"
class="w-full px-4 py-3 rounded-xl border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700/50 text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 shadow-sm transition-colors duration-200">
</div>
</div>
<div>
<label for="job_title" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Job-Titel*</label>
<input type="text" id="job_title" name="job_title" required placeholder="Geben Sie einen beschreibenden Titel ein"
class="w-full px-4 py-3 rounded-xl border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700/50 text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 shadow-sm transition-colors duration-200">
</div>
<div>
<label for="stl_file" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">STL-Datei hochladen (optional)</label>
<div class="flex items-center justify-center w-full">
<label for="stl_file" class="flex flex-col items-center justify-center w-full h-32 border-2 border-slate-300 dark:border-slate-600 border-dashed rounded-xl cursor-pointer bg-slate-50 dark:bg-slate-700/30 hover:bg-slate-100 dark:hover:bg-slate-700/50 transition-colors duration-200">
<div class="flex flex-col items-center justify-center pt-5 pb-6">
<svg class="w-10 h-10 mb-3 text-slate-500 dark:text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<p class="mb-2 text-sm text-slate-600 dark:text-slate-400"><span class="font-semibold">Klicken Sie zum Hochladen</span> oder ziehen Sie die Datei hierher</p>
<p class="text-xs text-slate-500 dark:text-slate-500">STL bis zu 50MB</p>
</div>
<input id="stl_file" name="stl_file" type="file" accept=".stl" class="hidden" />
</label>
</div>
<div id="file-name" class="mt-2 text-sm text-slate-500 dark:text-slate-400 hidden"></div>
</div>
<div class="flex justify-end pt-4">
<button type="submit" class="bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 text-white px-8 py-3 rounded-xl font-medium transition-colors duration-300">
Reservierung erstellen
</button>
</div>
</form>
</div>
<!-- Aktive und geplante Jobs -->
<div class="relative overflow-hidden rounded-2xl p-6 bg-white/70 dark:bg-slate-800/70 backdrop-blur-lg border border-gray-200/80 dark:border-slate-700/30 shadow-xl transition-all duration-300">
<h2 class="text-xl font-semibold mb-6 text-slate-900 dark:text-white">Aktive und geplante Jobs</h2>
<div id="active-jobs-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Jobs werden dynamisch mit JavaScript geladen -->
<div class="col-span-full py-12 text-center bg-slate-50 dark:bg-slate-700/30 rounded-xl" id="no-jobs-message">
<svg class="w-16 h-16 mx-auto mb-4 text-slate-400 dark:text-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
</svg>
<h3 class="text-lg font-medium text-slate-900 dark:text-white mb-1">Keine aktiven Jobs</h3>
<p class="text-slate-500 dark:text-slate-400">Sie haben derzeit keine aktiven oder geplanten Druckjobs.</p>
</div>
</div>
</div>
</div>
<!-- Job Details Modal -->
<div id="jobDetailsModal" class="fixed inset-0 z-50 flex items-center justify-center hidden">
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm" onclick="closeJobModal()"></div>
<div class="relative bg-white dark:bg-slate-800 rounded-2xl shadow-2xl w-full max-w-2xl max-h-[80vh] overflow-auto">
<button class="absolute top-4 right-4 text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200" onclick="closeJobModal()">
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<div class="p-6" id="jobDetailsContent">
<!-- Inhalt wird dynamisch geladen -->
<div class="animate-pulse">
<div class="h-6 bg-slate-200 dark:bg-slate-700 rounded w-3/4 mb-4"></div>
<div class="h-4 bg-slate-200 dark:bg-slate-700 rounded w-1/2 mb-8"></div>
<div class="grid grid-cols-2 gap-4 mb-6">
<div class="h-8 bg-slate-200 dark:bg-slate-700 rounded"></div>
<div class="h-8 bg-slate-200 dark:bg-slate-700 rounded"></div>
<div class="h-8 bg-slate-200 dark:bg-slate-700 rounded"></div>
<div class="h-8 bg-slate-200 dark:bg-slate-700 rounded"></div>
</div>
<div class="h-32 bg-slate-200 dark:bg-slate-700 rounded mb-6"></div>
<div class="h-10 bg-slate-200 dark:bg-slate-700 rounded w-1/3"></div>
</div>
</div>
</div>
</div>
<!-- Job verlängern Modal -->
<div id="extendJobModal" class="fixed inset-0 z-50 flex items-center justify-center hidden">
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm" onclick="closeExtendModal()"></div>
<div class="relative bg-white dark:bg-slate-800 rounded-2xl shadow-2xl w-full max-w-md">
<button class="absolute top-4 right-4 text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200" onclick="closeExtendModal()">
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<div class="p-6">
<h3 class="text-xl font-bold mb-4 text-slate-900 dark:text-white">Druckzeit verlängern</h3>
<p class="mb-6 text-slate-600 dark:text-slate-400">Geben Sie an, um wie viele Minuten die Druckzeit verlängert werden soll.</p>
<form id="extendJobForm">
<input type="hidden" id="extend_job_id" name="job_id">
<div class="mb-6">
<label for="extra_minutes" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Zusätzliche Minuten</label>
<input type="number" id="extra_minutes" name="extra_minutes" required min="1" max="720" value="30"
class="w-full px-4 py-3 rounded-xl border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700/50 text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 shadow-sm transition-colors duration-200">
</div>
<div class="flex justify-end space-x-3">
<button type="button" onclick="closeExtendModal()"
class="px-4 py-2 border border-slate-300 dark:border-slate-600 rounded-lg text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors duration-200">
Abbrechen
</button>
<button type="submit"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 text-white rounded-lg transition-colors duration-200">
Verlängern
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// Globale Variable für Admin-Status
window.isAdmin = {% if current_user.is_admin %}true{% else %}false{% endif %};
</script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// File upload preview
const fileInput = document.getElementById('stl_file');
const fileNameDisplay = document.getElementById('file-name');
if (fileInput) {
fileInput.addEventListener('change', function() {
if (this.files && this.files.length > 0) {
fileNameDisplay.textContent = 'Ausgewählte Datei: ' + this.files[0].name;
fileNameDisplay.classList.remove('hidden');
} else {
fileNameDisplay.classList.add('hidden');
}
});
}
// Zeige die aktuelle Zeit als Standardwert für Startzeit
const startTimeInput = document.getElementById('start_time');
if (startTimeInput) {
const now = new Date();
now.setMinutes(now.getMinutes() + 30); // Default: 30 Minuten in der Zukunft
startTimeInput.value = formatDateTimeForInput(now);
}
// Drucker-Liste laden und Formular initialisieren
loadPrinters();
loadActiveJobs();
// Event-Listener für Drucker-Auswahl (Status-Anzeige)
const printerSelect = document.getElementById('printer_id');
const statusWarning = document.getElementById('printer-status-warning');
const statusInfo = document.getElementById('printer-status-info');
if (printerSelect && statusWarning && statusInfo) {
printerSelect.addEventListener('change', function() {
const selectedOption = this.options[this.selectedIndex];
if (!selectedOption || selectedOption.value === "") {
// Keine Auswahl
statusWarning.classList.add('hidden');
statusInfo.innerHTML = '';
return;
}
// Status des ausgewählten Druckers bestimmen
const isOffline = selectedOption.getAttribute('data-offline') === 'true';
const printerName = selectedOption.textContent.split('(')[0].trim().replace(/🟢|🔴/g, '').trim();
if (isOffline) {
// Offline-Drucker: Deutliche Warnung anzeigen
statusWarning.classList.remove('hidden');
statusInfo.innerHTML = `
<div class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800 border border-red-200">
<span class="w-2 h-2 mr-1 bg-red-500 rounded-full"></span>
${printerName} ist OFFLINE
</div>
`;
} else {
// Online-Drucker: Positive Statusmeldung
statusWarning.classList.add('hidden');
statusInfo.innerHTML = `
<div class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800 border border-green-200">
<span class="w-2 h-2 mr-1 bg-green-500 rounded-full animate-pulse"></span>
${printerName} ist ONLINE und bereit
</div>
`;
}
});
// Initial auslösen, um den Status der Vorauswahl anzuzeigen
printerSelect.dispatchEvent(new Event('change'));
}
// Formulare initialisieren
initNewJobForm();
initExtendJobForm();
// Timer für automatische Aktualisierung der Jobs (alle 60 Sekunden)
setInterval(loadActiveJobs, 60000);
// Timer für Überprüfung wartender Jobs (alle 30 Sekunden)
setInterval(checkWaitingJobs, 30000);
});
// Hilfsfunktion zum Formatieren des Datums für Datetime-Input
function formatDateTimeForInput(date) {
const pad = (num) => num.toString().padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
}
// Aktualisiere die Job-Liste
function refreshJobs() {
const refreshButton = document.querySelector('button[onclick="refreshJobs()"]');
if (refreshButton) {
refreshButton.disabled = true;
refreshButton.innerHTML = `
<svg class="h-5 w-5 mr-2 inline animate-spin" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Aktualisieren...
`;
}
loadActiveJobs().then(() => {
if (refreshButton) {
refreshButton.disabled = false;
refreshButton.innerHTML = `
<svg class="h-5 w-5 mr-2 inline" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Aktualisieren
`;
}
}).catch(error => {
console.error('Fehler beim Aktualisieren der Jobs:', error);
showNotification('Fehler beim Aktualisieren der Jobs', 'error');
if (refreshButton) {
refreshButton.disabled = false;
refreshButton.innerHTML = `
<svg class="h-5 w-5 mr-2 inline" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Aktualisieren
`;
}
});
}
// Laden der Drucker für das Dropdown mit verbesserter Online-Erkennung
function loadPrinters() {
const printerSelect = document.getElementById('printer_id');
// Loading-State anzeigen
printerSelect.innerHTML = '<option value="">Lade Drucker...</option>';
printerSelect.disabled = true;
// Zuerst versuchen, Online-Drucker zu laden (schnell)
fetch('/api/printers/online')
.then(response => response.json())
.then(data => {
const onlinePrinters = data.printers || [];
console.log('Online-Drucker geladen:', onlinePrinters);
if (onlinePrinters.length > 0) {
// Online-Drucker verfügbar - diese bevorzugt anzeigen
populatePrinterSelect(onlinePrinters, true);
showNotification(`${onlinePrinters.length} online Drucker verfügbar`, 'success');
// Zusätzlich alle Drucker laden für Vollständigkeit
loadAllPrintersAsSecondary(onlinePrinters);
} else {
// Keine Online-Drucker - lade alle mit Live-Status-Check
loadPrintersWithLiveStatus();
}
})
.catch(error => {
console.error('Fehler beim Laden der Online-Drucker:', error);
// Fallback: Lade alle Drucker mit Live-Status
loadPrintersWithLiveStatus();
});
}
// Hilfsfunktion: Drucker-Select befüllen
function populatePrinterSelect(printers, onlineOnly = false) {
const printerSelect = document.getElementById('printer_id');
printerSelect.innerHTML = '<option value="">Drucker auswählen...</option>';
printerSelect.disabled = false;
if (printers.length === 0) {
printerSelect.innerHTML = '<option value="">Keine Drucker verfügbar</option>';
return;
}
// Sortiere Drucker: Online zuerst, dann nach Name
const sortedPrinters = printers.sort((a, b) => {
// Online-Status prüfen
const aOnline = a.status === 'available' || a.is_online || a.active;
const bOnline = b.status === 'available' || b.is_online || b.active;
if (aOnline && !bOnline) return -1;
if (!aOnline && bOnline) return 1;
// Bei gleichem Online-Status nach Name sortieren
return a.name.localeCompare(b.name);
});
// Zähler für Online- und Offline-Drucker
let onlineCount = 0;
let offlineCount = 0;
// Optionsgruppen für bessere visuelle Trennung erstellen
const onlineGroup = document.createElement('optgroup');
onlineGroup.label = "── ONLINE DRUCKER ──";
onlineGroup.style.fontWeight = 'bold';
onlineGroup.style.color = '#047857'; // Dunkelgrün
const offlineGroup = document.createElement('optgroup');
offlineGroup.label = "── OFFLINE DRUCKER ──";
offlineGroup.style.fontWeight = 'bold';
offlineGroup.style.color = '#b91c1c'; // Dunkelrot
// Drucker in entsprechende Gruppen einsortieren
sortedPrinters.forEach(printer => {
const option = document.createElement('option');
option.value = printer.id;
// Status-Indikator bestimmen
const isOnline = printer.status === 'available' || printer.is_online || printer.active;
let statusIcon, statusText, statusClass;
if (isOnline) {
statusIcon = '🟢';
statusText = 'ONLINE';
statusClass = 'online';
option.style.backgroundColor = 'rgba(4, 120, 87, 0.1)'; // Leicht grüner Hintergrund
option.style.color = '#047857'; // Grün für online
option.style.fontWeight = '500';
onlineCount++;
} else {
statusIcon = '🔴';
statusText = 'OFFLINE';
statusClass = 'offline';
option.style.backgroundColor = 'rgba(185, 28, 28, 0.05)'; // Sehr leicht roter Hintergrund
option.style.color = '#b91c1c'; // Rot für offline
option.style.fontStyle = 'italic';
option.style.fontWeight = '400';
// Offline-Drucker NICHT deaktivieren, aber kennzeichnen
option.setAttribute('data-offline', 'true');
offlineCount++;
}
// Letzter Check-Zeitstempel
let lastChecked = '';
if (printer.last_checked) {
const checkTime = new Date(printer.last_checked);
const now = new Date();
const diffMinutes = Math.floor((now - checkTime) / 60000);
if (diffMinutes < 1) {
lastChecked = ' (gerade geprüft)';
} else if (diffMinutes < 60) {
lastChecked = ` (vor ${diffMinutes} Min)`;
} else {
lastChecked = ` (vor ${Math.floor(diffMinutes / 60)} Std)`;
}
}
option.textContent = `${statusIcon} ${printer.name} (${printer.model || 'Unbekanntes Modell'}) - ${statusText}${lastChecked}`;
// Tooltip für zusätzliche Informationen
option.title = `Standort: ${printer.location || 'Unbekannt'}\nIP: ${printer.plug_ip || printer.ip_address || 'Unbekannt'}\nStatus: ${statusText}${lastChecked}`;
// Zu entsprechender Gruppe hinzufügen
if (isOnline) {
onlineGroup.appendChild(option);
} else {
offlineGroup.appendChild(option);
}
});
// Status-Info als erste Option hinzufügen
const statusOption = document.createElement('option');
statusOption.disabled = true;
statusOption.className = 'status-summary';
statusOption.style.backgroundColor = '#f3f4f6';
statusOption.style.fontWeight = 'bold';
statusOption.style.textAlign = 'center';
statusOption.style.padding = '4px';
statusOption.style.marginBottom = '4px';
statusOption.style.borderBottom = '1px solid #d1d5db';
// Deutliche Status-Zusammenfassung
if (onlineCount > 0) {
statusOption.textContent = `${onlineCount} von ${printers.length} Drucker ONLINE`;
statusOption.style.color = '#047857'; // Grün
} else {
statusOption.textContent = `⚠️ ACHTUNG: Alle ${printers.length} Drucker OFFLINE`;
statusOption.style.color = '#b91c1c'; // Rot
}
printerSelect.appendChild(statusOption);
// Gruppen zum Select hinzufügen
if (onlineCount > 0) {
printerSelect.appendChild(onlineGroup);
}
if (offlineCount > 0) {
printerSelect.appendChild(offlineGroup);
}
// Wenn online Drucker verfügbar, den ersten online Drucker vorauswählen
if (onlineCount > 0 && onlineGroup.firstChild) {
onlineGroup.firstChild.selected = true;
document.getElementById('printer-status-warning').classList.add('hidden');
} else if (offlineCount > 0 && offlineGroup.firstChild) {
// Sonst den ersten offline Drucker vorauswählen und Warnung anzeigen
offlineGroup.firstChild.selected = true;
document.getElementById('printer-status-warning').classList.remove('hidden');
}
}
// Alle Drucker als sekundäre Option laden
function loadAllPrintersAsSecondary(onlinePrinters) {
fetch('/api/printers/status/live')
.then(response => response.json())
.then(data => {
const allPrinters = data.printers || [];
// Prüfe, ob es zusätzliche Drucker gibt, die nicht in der Online-Liste sind
const onlineIds = new Set(onlinePrinters.map(p => p.id));
const additionalPrinters = allPrinters.filter(p => !onlineIds.has(p.id));
if (additionalPrinters.length > 0) {
// Kombiniere Online- und zusätzliche Drucker
const combinedPrinters = [...onlinePrinters, ...additionalPrinters];
populatePrinterSelect(combinedPrinters, false);
const totalOnline = combinedPrinters.filter(p => p.status === 'available' || p.is_online || p.active).length;
showNotification(`${totalOnline} von ${combinedPrinters.length} Drucker online`, totalOnline > 0 ? 'success' : 'warning');
}
})
.catch(error => {
console.error('Fehler beim Laden aller Drucker:', error);
// Nicht kritisch, Online-Drucker sind bereits geladen
});
}
// Drucker mit Live-Status-Check laden (Fallback)
function loadPrintersWithLiveStatus() {
showNotification('Überprüfe Drucker-Status...', 'info');
fetch('/api/printers/status/live')
.then(response => response.json())
.then(data => {
const printers = data.printers || [];
console.log('Live-Status-Drucker geladen:', printers);
if (printers.length === 0) {
// Letzter Fallback: Normale Drucker-API
return loadPrintersBasic();
}
populatePrinterSelect(printers, false);
// Verwende die vom Backend bereitgestellten Werte
const onlineCount = data.online_count || 0;
const totalCount = data.count || printers.length;
if (onlineCount > 0) {
if (onlineCount === totalCount) {
// Alle Drucker online
showNotification(`✅ OPTIMAL: Alle ${totalCount} Drucker sind ONLINE und BEREIT`, 'success');
} else {
// Einige Drucker online
const offlineCount = totalCount - onlineCount;
showNotification(`⚠️ ${onlineCount} von ${totalCount} Drucker ONLINE | ${offlineCount} Drucker OFFLINE`, 'success');
}
} else {
// Kein Drucker online
showNotification(`❌ ACHTUNG: Alle ${totalCount} Drucker sind OFFLINE - Reservierte Jobs werden in Warteschlange gespeichert`, 'error');
}
})
.catch(error => {
console.error('Fehler beim Live-Status-Check:', error);
showNotification('Live-Status-Check fehlgeschlagen, lade Basis-Daten...', 'warning');
loadPrintersBasic();
});
}
// Basis-Drucker-Laden (letzter Fallback)
function loadPrintersBasic() {
fetch('/api/printers')
.then(response => response.json())
.then(data => {
const printers = data.printers || [];
console.log('Basis-Drucker geladen:', printers);
if (printers.length === 0) {
const printerSelect = document.getElementById('printer_id');
printerSelect.innerHTML = '<option value="">Keine Drucker in der Datenbank</option>';
printerSelect.disabled = true;
showNotification('Keine Drucker in der Datenbank gefunden', 'error');
return;
}
// Alle Drucker als "Status unbekannt" anzeigen
const printersWithUnknownStatus = printers.map(printer => ({
...printer,
status: 'unknown',
is_online: false,
active: true // Erlaube Auswahl trotz unbekanntem Status
}));
populatePrinterSelect(printersWithUnknownStatus, false);
showNotification(`${printers.length} Drucker geladen (Status unbekannt)`, 'warning');
})
.catch(error => {
console.error('Auch Basis-API fehlgeschlagen:', error);
const printerSelect = document.getElementById('printer_id');
printerSelect.innerHTML = '<option value="">Fehler beim Laden der Drucker</option>';
printerSelect.disabled = true;
showNotification('Fehler beim Laden der Drucker', 'error');
});
}
// Laden der aktiven Jobs
function loadActiveJobs() {
fetch('/api/jobs/active')
.then(response => response.json())
.then(data => {
renderActiveJobs(data.jobs);
})
.catch(error => {
console.error('Fehler beim Laden der aktiven Jobs:', error);
showNotification('Fehler beim Laden der aktiven Jobs', 'error');
});
}
// Überprüfung wartender Jobs
function checkWaitingJobs() {
fetch('/api/jobs/check-waiting', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.updated_jobs && data.updated_jobs.length > 0) {
// Benachrichtigung für aktivierte Jobs
data.updated_jobs.forEach(job => {
showNotification(
`🎉 Gute Nachrichten! Drucker "${job.printer_name}" ist online. Ihr Job "${job.name}" wurde aktiviert und startet bald.`,
'success'
);
});
// Jobs neu laden, um aktualisierte Status anzuzeigen
loadActiveJobs();
}
})
.catch(error => {
console.error('Fehler beim Überprüfen wartender Jobs:', error);
// Stille Fehler - nicht den Benutzer stören
});
}
// Initialisierung des Formulars für neue Jobs
function initNewJobForm() {
const form = document.getElementById('newJobForm');
form.addEventListener('submit', function(e) {
e.preventDefault();
// Daten sammeln
const printer_id = document.getElementById('printer_id').value;
const start_time = document.getElementById('start_time').value;
const duration = document.getElementById('duration').value;
const job_title = document.getElementById('job_title').value;
const fileInput = document.getElementById('stl_file');
if (!printer_id || !start_time || !duration || !job_title) {
showNotification('Bitte füllen Sie alle Pflichtfelder aus', 'warning');
return;
}
// Prüfen, ob ein Offline-Drucker ausgewählt wurde
const selectedOption = document.querySelector(`#printer_id option[value="${printer_id}"]`);
if (selectedOption && selectedOption.getAttribute('data-offline') === 'true') {
const printerName = selectedOption.textContent.split(' (')[0].replace(/🔴\s*/g, '').trim();
// Deutlichere Warnung mit einem ausführlicheren Dialog
const confirmOffline = confirm(
`⛔ ACHTUNG: OFFLINE-DRUCKER AUSGEWÄHLT! ⛔\n` +
`---------------------------------------------\n\n` +
`Der Drucker "${printerName}" ist DERZEIT NICHT VERFÜGBAR!\n\n` +
`Wenn Sie trotzdem fortfahren:\n\n` +
`✓ Ihr Job wird in der WARTESCHLANGE gespeichert\n` +
`✓ Der System-Status wird regelmäßig überprüft\n` +
`✓ Sie erhalten eine BENACHRICHTIGUNG, sobald der Drucker online geht\n` +
`✓ Der Job startet AUTOMATISCH, wenn der Drucker verfügbar wird\n\n` +
`---------------------------------------------\n` +
`Möchten Sie TROTZDEM mit diesem Offline-Drucker fortfahren?`
);
if (!confirmOffline) {
showNotification('Job-Erstellung abgebrochen - Bitte wählen Sie einen ONLINE-Drucker für sofortigen Start', 'info');
return;
}
// Spezielle Benachrichtigung für Offline-Drucker-Jobs
showNotification(`⏳ Job für OFFLINE-Drucker "${printerName}" wird in Warteschlange erstellt. Sie werden benachrichtigt, wenn der Drucker verfügbar wird.`, 'warning');
}
// Startzeit in ISO-Format konvertieren
const start_date = new Date(start_time);
// Dateiupload, falls vorhanden
let file_path = null;
let formData = null;
if (fileInput.files && fileInput.files.length > 0) {
formData = new FormData();
formData.append('file', fileInput.files[0]);
// Zuerst Datei hochladen, dann Job erstellen
fetch('/api/files/upload', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(fileData => {
file_path = fileData.file_path;
createJob(printer_id, start_date.toISOString(), parseInt(duration), job_title, file_path);
})
.catch(error => {
console.error('Fehler beim Hochladen der Datei:', error);
showNotification('Fehler beim Hochladen der Datei', 'error');
});
} else {
// Direktes Erstellen des Jobs ohne Dateiupload
createJob(printer_id, start_date.toISOString(), parseInt(duration), job_title, null);
}
});
}
// Job erstellen
function createJob(printer_id, start_iso, duration_minutes, name, file_path) {
const jobData = {
printer_id: printer_id,
start_iso: start_iso,
duration_minutes: duration_minutes,
name: name
};
if (file_path) {
jobData.file_path = file_path;
}
fetch('/api/jobs', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(jobData)
})
.then(response => response.json())
.then(data => {
if (data.job) {
showNotification('Reservierung erfolgreich erstellt', 'success');
resetNewJobForm();
loadActiveJobs();
} else {
showNotification(data.error || 'Fehler beim Erstellen der Reservierung', 'error');
}
})
.catch(error => {
console.error('Fehler beim Erstellen des Jobs:', error);
showNotification('Fehler beim Erstellen der Reservierung', 'error');
});
}
// Formular zurücksetzen
function resetNewJobForm() {
document.getElementById('newJobForm').reset();
document.getElementById('file-name').classList.add('hidden');
// Startzeit zurücksetzen (30 Minuten in der Zukunft)
const startTimeInput = document.getElementById('start_time');
const now = new Date();
now.setMinutes(now.getMinutes() + 30);
startTimeInput.value = formatDateTimeForInput(now);
}
// Initialisierung des Formulars zum Verlängern von Jobs
function initExtendJobForm() {
const form = document.getElementById('extendJobForm');
form.addEventListener('submit', function(e) {
e.preventDefault();
const job_id = document.getElementById('extend_job_id').value;
const extra_minutes = document.getElementById('extra_minutes').value;
if (!job_id || !extra_minutes || parseInt(extra_minutes) <= 0) {
showNotification('Bitte geben Sie eine gültige Anzahl an Minuten ein', 'warning');
return;
}
extendJob(job_id, parseInt(extra_minutes));
});
}
// Notification/Toast anzeigen
function showNotification(message, type = 'info') {
// Prüfen, ob der Flash-Container existiert
let container = document.getElementById('flash-messages');
if (!container) {
// Flash-Container erstellen, falls nicht vorhanden
container = document.createElement('div');
container.id = 'flash-messages';
document.querySelector('main').prepend(container);
}
// Notification erstellen
const notification = document.createElement('div');
notification.className = `flash-message ${type} mb-4`;
// Icon basierend auf Typ
let icon = '';
if (type === 'success') {
icon = '<svg class="w-5 h-5 mr-3 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>';
} else if (type === 'error') {
icon = '<svg class="w-5 h-5 mr-3 text-red-500" 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"></path></svg>';
} else if (type === 'warning') {
icon = '<svg class="w-5 h-5 mr-3 text-yellow-500" 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-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>';
} else {
icon = '<svg class="w-5 h-5 mr-3 text-blue-500" 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"></path></svg>';
}
// HTML setzen
notification.innerHTML = `
<div class="flex items-center">
${icon}
<p class="text-sm">${message}</p>
</div>
<button class="absolute top-2 right-2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300">
<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"></path>
</svg>
</button>
`;
// Close-Button
notification.querySelector('button').addEventListener('click', function() {
notification.remove();
});
// Zum Container hinzufügen
container.appendChild(notification);
// Nach 5 Sekunden automatisch entfernen
setTimeout(() => {
if (notification.parentNode) {
notification.style.opacity = '0';
notification.style.transform = 'translateY(-20px)';
setTimeout(() => {
if (notification.parentNode) {
notification.remove();
}
}, 300);
}
}, 5000);
}
// Job verlängern
function extendJob(job_id, extra_minutes) {
fetch(`/api/jobs/${job_id}/extend`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ extra_minutes: extra_minutes })
})
.then(response => response.json())
.then(data => {
if (data.job) {
showNotification('Job erfolgreich verlängert', 'success');
closeExtendModal();
loadActiveJobs();
} else {
showNotification(data.error || 'Fehler beim Verlängern des Jobs', 'error');
}
})
.catch(error => {
console.error('Fehler beim Verlängern des Jobs:', error);
showNotification('Fehler beim Verlängern des Jobs', 'error');
});
}
// Job manuell beenden (nur für Admins)
function finishJob(job_id) {
if (!confirm('Sind Sie sicher, dass Sie diesen Job beenden möchten? Die Steckdose wird sofort ausgeschaltet.')) {
return;
}
fetch(`/api/jobs/${job_id}/finish`, {
method: 'POST'
})
.then(response => response.json())
.then(data => {
if (data.job) {
showNotification('Job erfolgreich beendet', 'success');
loadActiveJobs();
} else {
showNotification(data.error || 'Fehler beim Beenden des Jobs', 'error');
}
})
.catch(error => {
console.error('Fehler beim Beenden des Jobs:', error);
showNotification('Fehler beim Beenden des Jobs', 'error');
});
}
// Job-Details anzeigen
function viewJobDetails(job_id) {
fetch(`/api/jobs/${job_id}`)
.then(response => response.json())
.then(job => {
renderJobDetails(job);
openJobModal();
})
.catch(error => {
console.error('Fehler beim Laden der Job-Details:', error);
showNotification('Fehler beim Laden der Job-Details', 'error');
});
}
// Job-Modal öffnen
function openJobModal() {
document.getElementById('jobDetailsModal').classList.remove('hidden');
document.body.classList.add('overflow-hidden');
}
// Job-Modal schließen
function closeJobModal() {
document.getElementById('jobDetailsModal').classList.add('hidden');
document.body.classList.remove('overflow-hidden');
}
// Modal zum Verlängern eines Jobs öffnen
function openExtendModal(job_id) {
document.getElementById('extend_job_id').value = job_id;
document.getElementById('extendJobModal').classList.remove('hidden');
document.body.classList.add('overflow-hidden');
document.getElementById('extra_minutes').focus();
}
// Modal zum Verlängern schließen
function closeExtendModal() {
document.getElementById('extendJobModal').classList.add('hidden');
document.body.classList.remove('overflow-hidden');
}
// Job-Karte rendern (für die Anzeige aktiver Jobs)
function renderJobCard(job) {
const now = new Date();
const startAt = new Date(job.start_at);
const endAt = new Date(job.end_at);
// Status-Badge bestimmen
let statusBadge = '';
if (job.status === 'running') {
statusBadge = `
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-400">
<span class="w-2 h-2 mr-1 bg-green-500 rounded-full animate-pulse"></span>
Aktiv
</span>
`;
} else if (job.status === 'scheduled') {
// Prüfe, ob der Drucker online ist
const printerOnline = job.printer?.status === 'available' || job.printer?.active;
if (printerOnline) {
statusBadge = `
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-400">
<span class="w-2 h-2 mr-1 bg-blue-500 rounded-full"></span>
Geplant
</span>
`;
} else {
statusBadge = `
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-orange-100 dark:bg-orange-900/30 text-orange-800 dark:text-orange-400">
<span class="w-2 h-2 mr-1 bg-orange-500 rounded-full animate-pulse"></span>
Wartend auf Drucker
</span>
`;
}
} else if (job.status === 'waiting_for_printer') {
statusBadge = `
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-400">
<span class="w-2 h-2 mr-1 bg-yellow-500 rounded-full animate-pulse"></span>
Drucker offline
</span>
`;
}
// Restzeit oder Zeit bis Start berechnen
let timeInfo = '';
let progressBar = '';
if (job.status === 'running') {
// Restzeit für laufende Jobs
const remainingMinutes = job.remaining_minutes || Math.max(0, Math.floor((endAt - now) / 60000));
const hours = Math.floor(remainingMinutes / 60);
const minutes = remainingMinutes % 60;
const timeDisplay = hours > 0
? `${hours} Std. ${minutes} Min.`
: `${minutes} Minuten`;
timeInfo = `
<div class="mb-4">
<div class="flex justify-between text-xs text-slate-500 dark:text-slate-400 mb-1">
<span>Restzeit:</span>
<span>${timeDisplay}</span>
</div>
</div>
`;
// Fortschrittsbalken
const totalDuration = job.duration_minutes;
const elapsed = totalDuration - remainingMinutes;
const progress = Math.min(100, Math.max(0, Math.round((elapsed / totalDuration) * 100)));
progressBar = `
<div class="w-full bg-slate-200 dark:bg-slate-700 rounded-full h-2.5 mb-4">
<div class="bg-blue-600 h-2.5 rounded-full" style="width: ${progress}%"></div>
</div>
`;
} else if (job.status === 'scheduled') {
// Zeit bis zum Start für geplante Jobs
const minutesToStart = Math.max(0, Math.floor((startAt - now) / 60000));
const hours = Math.floor(minutesToStart / 60);
const minutes = minutesToStart % 60;
const timeDisplay = hours > 0
? `${hours} Std. ${minutes} Min.`
: `${minutes} Minuten`;
timeInfo = `
<div class="mb-4">
<div class="flex justify-between text-xs text-slate-500 dark:text-slate-400 mb-1">
<span>Startet in:</span>
<span>${timeDisplay}</span>
</div>
</div>
`;
}
// Aktionsbuttons
let actionButtons = `
<button type="button" onclick="viewJobDetails(${job.id})"
class="text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white text-sm font-medium transition-colors duration-200">
Details
</button>
`;
// Verlängern-Button für laufende und geplante Jobs
if (job.status === 'running' || job.status === 'scheduled') {
actionButtons += `
<button type="button" onclick="openExtendModal(${job.id})"
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 text-sm font-medium transition-colors duration-200">
Verlängern
</button>
`;
}
// Admin: Beenden-Button für laufende Jobs
if (job.status === 'running' && window.isAdmin) {
actionButtons += `
<button type="button" onclick="finishJob(${job.id})"
class="text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 text-sm font-medium transition-colors duration-200">
Beenden
</button>
`;
}
// Drucker-Icon basierend auf Status
const printerStatus = job.status === 'running' ? 'online' : 'idle';
const statusDot = job.status === 'running'
? '<span class="absolute bottom-0 right-0 block h-2.5 w-2.5 rounded-full bg-green-500 dark:bg-green-400 ring-2 ring-white dark:ring-slate-700"></span>'
: '<span class="absolute bottom-0 right-0 block h-2.5 w-2.5 rounded-full bg-blue-500 dark:bg-blue-400 ring-2 ring-white dark:ring-slate-700"></span>';
// Vollständige Job-Karte
const card = document.createElement('div');
card.className = 'relative overflow-hidden rounded-xl bg-slate-50 dark:bg-slate-700/30 border border-slate-200 dark:border-slate-600/30 shadow-md hover:shadow-lg transition-all duration-300';
card.innerHTML = `
<div class="absolute top-4 right-4">
${statusBadge}
</div>
<div class="p-5">
<h3 class="text-lg font-bold mb-2 pr-24 text-slate-900 dark:text-white">${job.name}</h3>
<div class="flex items-center text-sm text-slate-500 dark:text-slate-400 mb-4">
<svg class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<span>${formatDateTime(job.start_at)}</span>
<svg class="h-4 w-4 ml-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>${job.duration_minutes} Min.</span>
</div>
<div class="flex items-center mb-4">
<div class="mr-3 flex-shrink-0">
<div class="relative">
<svg class="h-10 w-10 text-slate-700 dark:text-slate-300 bg-white dark:bg-slate-600 p-1.5 rounded-lg shadow-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z" />
</svg>
${statusDot}
</div>
</div>
<div>
<p class="text-sm font-medium text-slate-900 dark:text-white">${job.printer?.name || 'Unbekannter Drucker'}</p>
<p class="text-xs text-slate-500 dark:text-slate-400">${job.printer?.model || ''}</p>
</div>
</div>
${progressBar}
${timeInfo}
<div class="flex justify-between pt-3 border-t border-slate-200 dark:border-slate-600/30 mt-4">
${actionButtons}
</div>
</div>
`;
return card;
}
// Job-Details rendern
function renderJobDetails(job) {
const detailsContainer = document.getElementById('jobDetailsContent');
const startAt = new Date(job.start_at);
const endAt = new Date(job.end_at);
const duration = job.duration_minutes;
// Status-Label
let statusLabel = '';
if (job.status === 'running') {
statusLabel = '<span class="bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-400 px-2.5 py-1 rounded-full text-xs font-medium">Aktiv</span>';
} else if (job.status === 'scheduled') {
statusLabel = '<span class="bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-400 px-2.5 py-1 rounded-full text-xs font-medium">Geplant</span>';
} else if (job.status === 'finished') {
statusLabel = '<span class="bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-400 px-2.5 py-1 rounded-full text-xs font-medium">Abgeschlossen</span>';
} else if (job.status === 'aborted') {
statusLabel = '<span class="bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-400 px-2.5 py-1 rounded-full text-xs font-medium">Abgebrochen</span>';
}
// Restzeit oder Zeit bis Start berechnen
const now = new Date();
let timeInfo = '';
if (job.status === 'running') {
const remainingMinutes = Math.max(0, Math.floor((endAt - now) / 60000));
const hours = Math.floor(remainingMinutes / 60);
const minutes = remainingMinutes % 60;
const timeDisplay = hours > 0
? `${hours} Stunden, ${minutes} Minuten`
: `${minutes} Minuten`;
timeInfo = `<p class="text-sm text-slate-600 dark:text-slate-400 mb-4">Verbleibende Zeit: <span class="font-semibold">${timeDisplay}</span></p>`;
} else if (job.status === 'scheduled') {
const minutesToStart = Math.max(0, Math.floor((startAt - now) / 60000));
const hours = Math.floor(minutesToStart / 60);
const minutes = minutesToStart % 60;
const timeDisplay = hours > 0
? `${hours} Stunden, ${minutes} Minuten`
: `${minutes} Minuten`;
timeInfo = `<p class="text-sm text-slate-600 dark:text-slate-400 mb-4">Startet in: <span class="font-semibold">${timeDisplay}</span></p>`;
}
// Aktionsbuttons
let actionButtons = '';
if (job.status === 'running' || job.status === 'scheduled') {
actionButtons += `
<button type="button" onclick="openExtendModal(${job.id}); closeJobModal();"
class="ml-3 bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 text-white px-4 py-2 rounded-lg transition-colors duration-200">
Verlängern
</button>
`;
}
if (job.status === 'running' && isAdmin) {
actionButtons += `
<button type="button" onclick="finishJob(${job.id}); closeJobModal();"
class="ml-3 bg-red-600 hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-500 text-white px-4 py-2 rounded-lg transition-colors duration-200">
Beenden
</button>
`;
}
// Datei-Informationen
let fileInfo = '';
if (job.file_path) {
const fileName = job.file_path.split('/').pop();
fileInfo = `
<div class="mb-6 p-4 bg-slate-100 dark:bg-slate-700 rounded-lg">
<div class="flex items-center">
<svg class="w-6 h-6 text-slate-500 dark:text-slate-400 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<div>
<p class="text-sm font-medium text-slate-900 dark:text-white">${fileName}</p>
<p class="text-xs text-slate-500 dark:text-slate-400">STL-Datei</p>
</div>
<a href="/api/files/download?path=${encodeURIComponent(job.file_path)}"
class="ml-auto bg-slate-200 hover:bg-slate-300 dark:bg-slate-600 dark:hover:bg-slate-500 text-slate-800 dark:text-slate-200 px-3 py-1 text-sm rounded-lg transition-colors duration-200">
Herunterladen
</a>
</div>
</div>
`;
}
// HTML für die Job-Details zusammenbauen
detailsContainer.innerHTML = `
<div class="flex justify-between items-start mb-4">
<h2 class="text-xl font-bold text-slate-900 dark:text-white">${job.name}</h2>
${statusLabel}
</div>
${timeInfo}
<div class="grid grid-cols-2 gap-4 mb-6">
<div>
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-1">Drucker</h3>
<p class="text-sm font-semibold text-slate-900 dark:text-white">${job.printer?.name || 'Unbekannt'}</p>
</div>
<div>
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-1">Modell</h3>
<p class="text-sm font-semibold text-slate-900 dark:text-white">${job.printer?.model || 'Unbekannt'}</p>
</div>
<div>
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-1">Startzeit</h3>
<p class="text-sm font-semibold text-slate-900 dark:text-white">${formatDateTime(job.start_at)}</p>
</div>
<div>
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-1">Geplante Endzeit</h3>
<p class="text-sm font-semibold text-slate-900 dark:text-white">${formatDateTime(job.end_at)}</p>
</div>
<div>
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-1">Dauer</h3>
<p class="text-sm font-semibold text-slate-900 dark:text-white">${duration} Minuten</p>
</div>
<div>
<h3 class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-1">Erstellt von</h3>
<p class="text-sm font-semibold text-slate-900 dark:text-white">${job.user?.name || job.user?.email || 'Unbekannt'}</p>
</div>
</div>
${fileInfo}
<div class="flex justify-end mt-6 pt-4 border-t border-slate-200 dark:border-slate-700">
<button type="button" onclick="closeJobModal()"
class="bg-slate-200 hover:bg-slate-300 dark:bg-slate-700 dark:hover:bg-slate-600 text-slate-800 dark:text-slate-200 px-4 py-2 rounded-lg transition-colors duration-200">
Schließen
</button>
${actionButtons}
</div>
`;
}
// Aktive Jobs anzeigen
function renderActiveJobs(jobs) {
const container = document.getElementById('active-jobs-container');
const noJobsMessage = document.getElementById('no-jobs-message');
// Container leeren
while (container.firstChild) {
container.removeChild(container.firstChild);
}
// "Keine Jobs" Nachricht anzeigen, wenn keine Jobs vorhanden sind
if (!jobs || jobs.length === 0) {
container.appendChild(noJobsMessage);
return;
} else {
// Nachricht entfernen, falls Jobs vorhanden sind
if (noJobsMessage.parentNode === container) {
container.removeChild(noJobsMessage);
}
}
// Jobs sortieren: Laufende zuerst, dann geplante, dann nach Startzeit
jobs.sort((a, b) => {
// Laufende Jobs haben Priorität
if (a.status === 'running' && b.status !== 'running') return -1;
if (a.status !== 'running' && b.status === 'running') return 1;
// Dann nach Startzeit sortieren
return new Date(a.start_at) - new Date(b.start_at);
});
// Jobs anzeigen
jobs.forEach(job => {
const jobCard = renderJobCard(job);
container.appendChild(jobCard);
});
}
// Formatiert einen ISO-Datums-String in lesbares Datum/Uhrzeit
function formatDateTime(isoString) {
if (!isoString) return 'Unbekannt';
const date = new Date(isoString);
return date.toLocaleString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
// Globale Variable für Admin-Status wird über window.isAdmin gesetzt
</script>
{% endblock %}