1315 lines
60 KiB
HTML
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 %} |