📚 Improved backend structure & logs, added conflict manager 🌟
This commit is contained in:
parent
f2928b97fc
commit
3cab66efc8
Binary file not shown.
Binary file not shown.
@ -2765,3 +2765,4 @@ WHERE jobs.status = ?) AS anon_1]
|
||||
2025-06-02 10:56:52 - [app] app - [INFO] INFO - Dashboard-Refresh angefordert von User 1
|
||||
2025-06-02 10:56:52 - [app] app - [INFO] INFO - Dashboard-Refresh erfolgreich: {'active_jobs': 0, 'available_printers': 2, 'total_jobs': 16, 'pending_jobs': 0, 'success_rate': 0.0, 'completed_jobs': 0, 'failed_jobs': 0, 'cancelled_jobs': None, 'total_users': 1, 'online_printers': 0, 'offline_printers': 2}
|
||||
2025-06-02 10:56:52 - [app] app - [INFO] INFO - Dashboard-Refresh erfolgreich: {'active_jobs': 0, 'available_printers': None, 'total_jobs': 16, 'pending_jobs': 0, 'success_rate': 0.0, 'completed_jobs': 0, 'failed_jobs': 0, 'cancelled_jobs': 0, 'total_users': 1, 'online_printers': 0, 'offline_printers': 2}
|
||||
2025-06-02 14:25:35 - [app] app - [INFO] INFO - Optimierte SQLite-Engine erstellt: C:\Users\TTOMCZA.EMEA\Dev\Projektarbeit-MYP\backend\database\myp.db
|
||||
|
@ -27181,3 +27181,4 @@
|
||||
2025-06-02 10:57:11 - [scheduler] scheduler - [ERROR] ERROR - ❌ Fehler beim einschalten der Tapo-Steckdose 192.168.0.103: HTTPConnectionPool(host='192.168.0.103', port=80): Max retries exceeded with url: /app (Caused by ConnectTimeoutError(<urllib3.connection.HTTPConnection object at 0x000001826C79D260>, 'Connection to 192.168.0.103 timed out. (connect timeout=2)'))
|
||||
2025-06-02 10:57:11 - [scheduler] scheduler - [ERROR] ERROR - ❌ Konnte Steckdose für Sofort-Job 14 nicht einschalten
|
||||
2025-06-02 10:57:11 - [scheduler] scheduler - [INFO] INFO - ⚡ Starte Sofort-Job 15: test
|
||||
2025-06-02 14:25:36 - [scheduler] scheduler - [INFO] INFO - Task check_jobs registriert: Intervall 30s, Enabled: True
|
||||
|
@ -451,3 +451,7 @@
|
||||
2025-06-02 10:02:51 - [windows_fixes] windows_fixes - [INFO] INFO - ✅ Subprocess automatisch gepatcht für UTF-8 Encoding (run + Popen)
|
||||
2025-06-02 10:02:51 - [windows_fixes] windows_fixes - [INFO] INFO - ✅ Globaler subprocess-Patch angewendet
|
||||
2025-06-02 10:02:51 - [windows_fixes] windows_fixes - [INFO] INFO - ✅ Alle Windows-Fixes erfolgreich angewendet
|
||||
2025-06-02 14:25:35 - [windows_fixes] windows_fixes - [INFO] INFO - 🔧 Wende Windows-spezifische Fixes an...
|
||||
2025-06-02 14:25:35 - [windows_fixes] windows_fixes - [INFO] INFO - ✅ Subprocess automatisch gepatcht für UTF-8 Encoding (run + Popen)
|
||||
2025-06-02 14:25:35 - [windows_fixes] windows_fixes - [INFO] INFO - ✅ Globaler subprocess-Patch angewendet
|
||||
2025-06-02 14:25:35 - [windows_fixes] windows_fixes - [INFO] INFO - ✅ Alle Windows-Fixes erfolgreich angewendet
|
||||
|
@ -117,17 +117,23 @@ check_system_resources() {
|
||||
local ram_mb=$(free -m | awk '/^Mem:/{print $2}')
|
||||
progress "Verfügbarer RAM: ${ram_mb}MB"
|
||||
|
||||
if [ "$ram_mb" -lt 512 ]; then
|
||||
if [ -n "$ram_mb" ] && [ "$ram_mb" -eq "$ram_mb" ] 2>/dev/null && [ "$ram_mb" -lt 512 ]; then
|
||||
warning "⚠️ Wenig RAM verfügbar (${ram_mb}MB) - Installation könnte langsam sein"
|
||||
else
|
||||
elif [ -n "$ram_mb" ] && [ "$ram_mb" -eq "$ram_mb" ] 2>/dev/null; then
|
||||
success "✅ Ausreichend RAM verfügbar (${ram_mb}MB)"
|
||||
else
|
||||
warning "⚠️ RAM-Größe konnte nicht ermittelt werden"
|
||||
fi
|
||||
|
||||
# Festplattenplatz prüfen
|
||||
local disk_free_gb=$(df / | awk 'NR==2{printf "%.1f", $4/1024/1024}')
|
||||
progress "Verfügbarer Festplattenplatz: ${disk_free_gb}GB"
|
||||
|
||||
if [ "$(echo "$disk_free_gb < 2.0" | bc 2>/dev/null || echo "0")" -eq 1 ]; then
|
||||
# Konvertiere zu Ganzzahl für Vergleich (GB * 10 für eine Dezimalstelle)
|
||||
local disk_free_int=$(echo "$disk_free_gb * 10" | bc 2>/dev/null | cut -d. -f1)
|
||||
local min_required_int=20 # 2.0 GB * 10
|
||||
|
||||
if [ -z "$disk_free_int" ] || [ "$disk_free_int" -lt "$min_required_int" ]; then
|
||||
warning "⚠️ Wenig Festplattenplatz verfügbar (${disk_free_gb}GB)"
|
||||
else
|
||||
success "✅ Ausreichend Festplattenplatz verfügbar (${disk_free_gb}GB)"
|
||||
|
741
backend/static/js/conflict-manager.js
Normal file
741
backend/static/js/conflict-manager.js
Normal file
@ -0,0 +1,741 @@
|
||||
/**
|
||||
* Erweiterte Druckerkonflikt-Management-Engine - Frontend
|
||||
* MYP Platform
|
||||
*
|
||||
* Behandelt Konflikte zwischen Druckerreservierungen mit
|
||||
* intelligenten Lösungsvorschlägen und Benutzerführung.
|
||||
*/
|
||||
|
||||
class ConflictManager {
|
||||
constructor() {
|
||||
this.lastConflictCheck = null;
|
||||
this.currentRecommendation = null;
|
||||
this.autoCheckEnabled = true;
|
||||
this.checkTimeout = null;
|
||||
this.debounceDelay = 500; // ms
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.createConflictModal();
|
||||
this.createAvailabilityPanel();
|
||||
this.createSmartRecommendationWidget();
|
||||
this.attachEventListeners();
|
||||
|
||||
console.log('🔧 ConflictManager initialisiert');
|
||||
}
|
||||
|
||||
// =================== MODAL CREATION ===================
|
||||
|
||||
createConflictModal() {
|
||||
const modalHTML = `
|
||||
<div id="conflictNotificationModal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50">
|
||||
<div class="flex items-center justify-center min-h-screen px-4">
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg max-w-2xl w-full max-h-96 overflow-y-auto">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white flex items-center">
|
||||
<svg class="w-6 h-6 mr-2 text-amber-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-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 15.5c-.77.833.192 2.5 1.732 2.5z"/>
|
||||
</svg>
|
||||
Druckerkonflikt erkannt
|
||||
</h3>
|
||||
<button id="closeConflictModal" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Konflikt-Zusammenfassung -->
|
||||
<div id="conflictSummary" class="mb-4 p-4 bg-amber-50 dark:bg-amber-900/20 rounded-lg border border-amber-200 dark:border-amber-800">
|
||||
<div class="flex items-center mb-2">
|
||||
<span id="conflictIcon" class="text-2xl mr-2">⚠️</span>
|
||||
<span id="conflictTitle" class="font-medium text-amber-800 dark:text-amber-200">Konflikte gefunden</span>
|
||||
</div>
|
||||
<p id="conflictDescription" class="text-amber-700 dark:text-amber-300 text-sm"></p>
|
||||
</div>
|
||||
|
||||
<!-- Detaillierte Konfliktliste -->
|
||||
<div id="conflictDetails" class="mb-4 space-y-3"></div>
|
||||
|
||||
<!-- Smart Empfehlungen -->
|
||||
<div id="smartRecommendations" class="mb-4"></div>
|
||||
|
||||
<!-- Aktionen -->
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button id="ignoreConflicts" class="px-4 py-2 text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200">
|
||||
Ignorieren
|
||||
</button>
|
||||
<button id="applyAutoFix" class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
Automatisch lösen
|
||||
</button>
|
||||
<button id="manualFix" class="px-4 py-2 bg-slate-600 text-white rounded-lg hover:bg-slate-700">
|
||||
Manuell anpassen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
||||
}
|
||||
|
||||
createAvailabilityPanel() {
|
||||
const panelHTML = `
|
||||
<div id="printerAvailabilityPanel" class="bg-white dark:bg-slate-800 rounded-lg shadow-lg p-4 mb-6 hidden">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white flex items-center">
|
||||
<svg class="w-5 h-5 mr-2 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 00-2-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
||||
</svg>
|
||||
Drucker-Verfügbarkeit
|
||||
</h3>
|
||||
<span id="availabilityTimestamp" class="text-sm text-slate-500 dark:text-slate-400"></span>
|
||||
</div>
|
||||
|
||||
<!-- Verfügbarkeits-Zusammenfassung -->
|
||||
<div id="availabilitySummary" class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
|
||||
<div class="text-center p-3 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
||||
<div id="totalPrinters" class="text-2xl font-bold text-green-600 dark:text-green-400">-</div>
|
||||
<div class="text-sm text-green-700 dark:text-green-300">Gesamt</div>
|
||||
</div>
|
||||
<div class="text-center p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
||||
<div id="availablePrinters" class="text-2xl font-bold text-blue-600 dark:text-blue-400">-</div>
|
||||
<div class="text-sm text-blue-700 dark:text-blue-300">Verfügbar</div>
|
||||
</div>
|
||||
<div class="text-center p-3 bg-emerald-50 dark:bg-emerald-900/20 rounded-lg">
|
||||
<div id="optimalPrinters" class="text-2xl font-bold text-emerald-600 dark:text-emerald-400">-</div>
|
||||
<div class="text-sm text-emerald-700 dark:text-emerald-300">Optimal</div>
|
||||
</div>
|
||||
<div class="text-center p-3 bg-slate-50 dark:bg-slate-700 rounded-lg">
|
||||
<div id="availabilityRate" class="text-2xl font-bold text-slate-600 dark:text-slate-400">-%</div>
|
||||
<div class="text-sm text-slate-700 dark:text-slate-300">Rate</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detaillierte Drucker-Liste -->
|
||||
<div id="printerDetailsList" class="space-y-2 max-h-64 overflow-y-auto"></div>
|
||||
</div>`;
|
||||
|
||||
// Panel nach dem Kalender-Container einfügen
|
||||
const calendarContainer = document.querySelector('.container');
|
||||
if (calendarContainer) {
|
||||
calendarContainer.insertAdjacentHTML('afterbegin', panelHTML);
|
||||
}
|
||||
}
|
||||
|
||||
createSmartRecommendationWidget() {
|
||||
const widgetHTML = `
|
||||
<div id="smartRecommendationWidget" class="bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 rounded-lg p-4 mb-6 border border-blue-200 dark:border-blue-800 hidden">
|
||||
<div class="flex items-start space-x-3">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-blue-100 dark:bg-blue-800 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-blue-900 dark:text-blue-100 mb-1">🎯 SMART-EMPFEHLUNG</p>
|
||||
<div id="recommendationContent">
|
||||
<p id="recommendationText" class="text-sm text-blue-800 dark:text-blue-200"></p>
|
||||
<div id="recommendationDetails" class="mt-2 text-xs text-blue-700 dark:text-blue-300 space-y-1"></div>
|
||||
</div>
|
||||
<div class="mt-3 flex space-x-2">
|
||||
<button id="acceptRecommendation" class="px-3 py-1 bg-blue-500 text-white text-xs rounded hover:bg-blue-600">
|
||||
Empfehlung annehmen
|
||||
</button>
|
||||
<button id="dismissRecommendation" class="px-3 py-1 text-blue-600 dark:text-blue-400 text-xs hover:text-blue-800 dark:hover:text-blue-200">
|
||||
Verwerfen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// Widget nach dem Kalender-Container einfügen
|
||||
const calendarContainer = document.querySelector('.container');
|
||||
if (calendarContainer) {
|
||||
calendarContainer.insertAdjacentHTML('afterbegin', widgetHTML);
|
||||
}
|
||||
}
|
||||
|
||||
// =================== EVENT LISTENERS ===================
|
||||
|
||||
attachEventListeners() {
|
||||
// Konflikt-Modal Events
|
||||
document.getElementById('closeConflictModal')?.addEventListener('click', () => {
|
||||
this.hideConflictModal();
|
||||
});
|
||||
|
||||
document.getElementById('applyAutoFix')?.addEventListener('click', () => {
|
||||
this.applyAutoFix();
|
||||
});
|
||||
|
||||
document.getElementById('ignoreConflicts')?.addEventListener('click', () => {
|
||||
this.ignoreConflicts();
|
||||
});
|
||||
|
||||
// Smart-Empfehlung Events
|
||||
document.getElementById('acceptRecommendation')?.addEventListener('click', () => {
|
||||
this.acceptRecommendation();
|
||||
});
|
||||
|
||||
document.getElementById('dismissRecommendation')?.addEventListener('click', () => {
|
||||
this.dismissRecommendation();
|
||||
});
|
||||
|
||||
// Formular-Validierung Events
|
||||
this.attachFormValidation();
|
||||
|
||||
// Verfügbarkeits-Button (falls vorhanden)
|
||||
document.getElementById('refreshAvailability')?.addEventListener('click', () => {
|
||||
this.refreshAvailability();
|
||||
});
|
||||
}
|
||||
|
||||
attachFormValidation() {
|
||||
const formFields = ['eventStart', 'eventEnd', 'eventPrinter', 'eventPriority'];
|
||||
|
||||
formFields.forEach(fieldId => {
|
||||
const field = document.getElementById(fieldId);
|
||||
if (field) {
|
||||
field.addEventListener('change', () => {
|
||||
this.scheduleValidation();
|
||||
});
|
||||
|
||||
field.addEventListener('input', () => {
|
||||
this.scheduleValidation();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// =================== API CALLS ===================
|
||||
|
||||
async checkConflicts(eventData) {
|
||||
try {
|
||||
const response = await fetch('/api/calendar/check-conflicts', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(eventData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
this.lastConflictCheck = result;
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler bei Konfliktprüfung:', error);
|
||||
this.showError('Konfliktprüfung fehlgeschlagen');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async resolveConflictsAndCreate(eventData) {
|
||||
try {
|
||||
const response = await fetch('/api/calendar/resolve-conflicts', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({...eventData, auto_resolve: true})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
if (response.status === 409) {
|
||||
this.showConflictModal(errorData);
|
||||
return null;
|
||||
} else {
|
||||
throw new Error(errorData.error || `HTTP ${response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler bei Konfliktlösung:', error);
|
||||
this.showError('Automatische Konfliktlösung fehlgeschlagen');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async loadPrinterAvailability(startTime, endTime) {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
start: startTime,
|
||||
end: endTime
|
||||
});
|
||||
|
||||
const response = await fetch(`/api/calendar/printer-availability?${params}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
this.displayAvailability(data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Laden der Verfügbarkeit:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async getSmartRecommendation(eventData) {
|
||||
try {
|
||||
const response = await fetch('/api/calendar/smart-recommendation', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(eventData)
|
||||
});
|
||||
|
||||
if (!response.ok) return null;
|
||||
|
||||
const recommendation = await response.json();
|
||||
if (recommendation.success) {
|
||||
this.displayRecommendation(recommendation.recommendation);
|
||||
this.currentRecommendation = recommendation.recommendation;
|
||||
return recommendation.recommendation;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler bei Smart-Empfehlung:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// =================== UI DISPLAY METHODS ===================
|
||||
|
||||
showConflictModal(conflictData) {
|
||||
const modal = document.getElementById('conflictNotificationModal');
|
||||
if (!modal) return;
|
||||
|
||||
this.updateConflictSummary(conflictData);
|
||||
this.updateConflictDetails(conflictData);
|
||||
this.updateConflictRecommendations(conflictData);
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
hideConflictModal() {
|
||||
const modal = document.getElementById('conflictNotificationModal');
|
||||
if (modal) {
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
updateConflictSummary(conflictData) {
|
||||
const title = document.getElementById('conflictTitle');
|
||||
const description = document.getElementById('conflictDescription');
|
||||
const icon = document.getElementById('conflictIcon');
|
||||
const summary = document.getElementById('conflictSummary');
|
||||
|
||||
if (!title || !description || !icon || !summary) return;
|
||||
|
||||
if (conflictData.severity_score >= 3) {
|
||||
icon.textContent = '🚨';
|
||||
title.textContent = 'Kritische Konflikte';
|
||||
summary.className = summary.className.replace(/amber/g, 'red');
|
||||
} else {
|
||||
icon.textContent = '⚠️';
|
||||
title.textContent = 'Konflikte erkannt';
|
||||
}
|
||||
|
||||
description.textContent = `${conflictData.conflict_count} Konflikt(e) gefunden. ${conflictData.can_proceed ? 'Automatische Lösung möglich.' : 'Manuelle Anpassung erforderlich.'}`;
|
||||
}
|
||||
|
||||
updateConflictDetails(conflictData) {
|
||||
const details = document.getElementById('conflictDetails');
|
||||
if (!details) return;
|
||||
|
||||
details.innerHTML = '';
|
||||
if (conflictData.conflicts) {
|
||||
conflictData.conflicts.forEach(conflict => {
|
||||
const conflictEl = this.createConflictElement(conflict);
|
||||
details.appendChild(conflictEl);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateConflictRecommendations(conflictData) {
|
||||
const recommendations = document.getElementById('smartRecommendations');
|
||||
if (!recommendations) return;
|
||||
|
||||
recommendations.innerHTML = '';
|
||||
if (conflictData.recommendations) {
|
||||
conflictData.recommendations.forEach(rec => {
|
||||
const recEl = this.createRecommendationElement(rec);
|
||||
recommendations.appendChild(recEl);
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-Fix Button aktivieren/deaktivieren
|
||||
const autoFixBtn = document.getElementById('applyAutoFix');
|
||||
if (autoFixBtn) {
|
||||
autoFixBtn.disabled = !conflictData.can_proceed;
|
||||
}
|
||||
}
|
||||
|
||||
createConflictElement(conflict) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'p-3 border border-slate-200 dark:border-slate-600 rounded-lg';
|
||||
|
||||
const severityColors = {
|
||||
'kritisch': 'red',
|
||||
'hoch': 'orange',
|
||||
'mittel': 'amber',
|
||||
'niedrig': 'blue',
|
||||
'information': 'slate'
|
||||
};
|
||||
|
||||
const color = severityColors[conflict.severity] || 'slate';
|
||||
|
||||
div.innerHTML = `
|
||||
<div class="flex items-start space-x-3">
|
||||
<span class="flex-shrink-0 w-2 h-2 mt-2 bg-${color}-500 rounded-full"></span>
|
||||
<div class="flex-1">
|
||||
<h4 class="font-medium text-slate-900 dark:text-white text-sm">${conflict.description}</h4>
|
||||
<p class="text-xs text-slate-600 dark:text-slate-400 mt-1">${conflict.estimated_impact}</p>
|
||||
${conflict.conflicting_jobs?.length > 0 ? `
|
||||
<div class="mt-2 text-xs text-slate-500 dark:text-slate-500">
|
||||
Betroffene Jobs: ${conflict.conflicting_jobs.map(job => job.name).join(', ')}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
createRecommendationElement(recommendation) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg';
|
||||
|
||||
let content = `
|
||||
<div class="flex items-start space-x-2">
|
||||
<span class="text-blue-500 text-sm">💡</span>
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-blue-800 dark:text-blue-200">${recommendation.message}</p>
|
||||
`;
|
||||
|
||||
if (recommendation.suggestions) {
|
||||
content += '<div class="mt-2 space-y-1">';
|
||||
recommendation.suggestions.forEach(suggestion => {
|
||||
content += `
|
||||
<div class="text-xs text-blue-700 dark:text-blue-300 flex items-center justify-between">
|
||||
<span>${suggestion.description || suggestion.printer_name}</span>
|
||||
${suggestion.confidence ? `<span class="text-blue-500">${Math.round(suggestion.confidence * 100)}%</span>` : ''}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
content += '</div>';
|
||||
}
|
||||
|
||||
content += `
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
div.innerHTML = content;
|
||||
return div;
|
||||
}
|
||||
|
||||
displayAvailability(data) {
|
||||
const panel = document.getElementById('printerAvailabilityPanel');
|
||||
if (!panel) return;
|
||||
|
||||
const summary = data.summary;
|
||||
|
||||
// Zusammenfassung aktualisieren
|
||||
this.updateElement('totalPrinters', summary.total_printers);
|
||||
this.updateElement('availablePrinters', summary.available_printers);
|
||||
this.updateElement('optimalPrinters', summary.optimal_printers);
|
||||
this.updateElement('availabilityRate', `${summary.availability_rate}%`);
|
||||
this.updateElement('availabilityTimestamp', `Aktualisiert: ${new Date().toLocaleTimeString()}`);
|
||||
|
||||
// Drucker-Details aktualisieren
|
||||
const detailsList = document.getElementById('printerDetailsList');
|
||||
if (detailsList) {
|
||||
detailsList.innerHTML = '';
|
||||
|
||||
data.printers.forEach(printer => {
|
||||
const printerEl = document.createElement('div');
|
||||
printerEl.className = 'flex items-center justify-between p-2 bg-slate-50 dark:bg-slate-700 rounded';
|
||||
|
||||
printerEl.innerHTML = `
|
||||
<div class="flex items-center space-x-3">
|
||||
<span class="text-lg">${printer.availability_icon}</span>
|
||||
<div>
|
||||
<div class="font-medium text-sm text-slate-900 dark:text-white">${printer.printer_name}</div>
|
||||
<div class="text-xs text-slate-500 dark:text-slate-400">${printer.location || 'Kein Standort'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-xs text-slate-600 dark:text-slate-400">${printer.status_description}</div>
|
||||
<div class="text-xs text-slate-500 dark:text-slate-500">${printer.recent_jobs_24h} Jobs (24h)</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
detailsList.appendChild(printerEl);
|
||||
});
|
||||
}
|
||||
|
||||
panel.classList.remove('hidden');
|
||||
}
|
||||
|
||||
displayRecommendation(recommendation) {
|
||||
const widget = document.getElementById('smartRecommendationWidget');
|
||||
if (!widget) return;
|
||||
|
||||
const text = document.getElementById('recommendationText');
|
||||
const details = document.getElementById('recommendationDetails');
|
||||
|
||||
if (text) {
|
||||
text.textContent = `Drucker: ${recommendation.printer_name}`;
|
||||
}
|
||||
|
||||
if (details) {
|
||||
details.innerHTML = `
|
||||
<div>📍 Standort: ${recommendation.location}</div>
|
||||
<div>📊 Verfügbarkeit: ${recommendation.availability}</div>
|
||||
<div>⚡ Auslastung: ${recommendation.utilization}</div>
|
||||
<div>🎯 Eignung: ${recommendation.suitability}</div>
|
||||
<div>💡 ${recommendation.reason}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
widget.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// =================== FORM VALIDATION ===================
|
||||
|
||||
scheduleValidation() {
|
||||
if (this.checkTimeout) {
|
||||
clearTimeout(this.checkTimeout);
|
||||
}
|
||||
|
||||
this.checkTimeout = setTimeout(() => {
|
||||
this.validateFormRealtime();
|
||||
}, this.debounceDelay);
|
||||
}
|
||||
|
||||
async validateFormRealtime() {
|
||||
if (!this.autoCheckEnabled) return;
|
||||
|
||||
const formData = this.getFormData();
|
||||
if (!formData.start_time || !formData.end_time) return;
|
||||
|
||||
// Formular-Hash für Debouncing
|
||||
const formHash = JSON.stringify(formData);
|
||||
if (this.lastFormHash === formHash) return;
|
||||
this.lastFormHash = formHash;
|
||||
|
||||
try {
|
||||
// Konflikte prüfen
|
||||
const conflicts = await this.checkConflicts(formData);
|
||||
if (conflicts) {
|
||||
this.showInlineConflictWarning(conflicts);
|
||||
}
|
||||
|
||||
// Smart-Empfehlung anzeigen (wenn kein Drucker ausgewählt)
|
||||
if (!formData.printer_id) {
|
||||
await this.getSmartRecommendation(formData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler bei Echzeit-Validierung:', error);
|
||||
}
|
||||
}
|
||||
|
||||
showInlineConflictWarning(conflictData) {
|
||||
// Inline-Warnung anzeigen (falls im Template vorhanden)
|
||||
const warning = document.getElementById('conflictWarning');
|
||||
if (!warning) return;
|
||||
|
||||
if (conflictData.has_conflicts) {
|
||||
// Warnung anzeigen
|
||||
warning.classList.remove('hidden');
|
||||
|
||||
// Nachrichten aktualisieren
|
||||
const messages = document.getElementById('conflictMessages');
|
||||
if (messages) {
|
||||
messages.innerHTML = '';
|
||||
conflictData.conflicts.forEach(conflict => {
|
||||
const msgEl = document.createElement('div');
|
||||
msgEl.textContent = `${conflict.severity}: ${conflict.description}`;
|
||||
messages.appendChild(msgEl);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
warning.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// =================== USER ACTIONS ===================
|
||||
|
||||
async applyAutoFix() {
|
||||
if (!this.lastConflictCheck) return;
|
||||
|
||||
const formData = this.getFormData();
|
||||
const result = await this.resolveConflictsAndCreate(formData);
|
||||
|
||||
if (result && result.success) {
|
||||
this.hideConflictModal();
|
||||
this.showSuccess('Konflikte automatisch gelöst und Auftrag erstellt! ✅');
|
||||
|
||||
// Kalender aktualisieren
|
||||
if (window.calendar) {
|
||||
window.calendar.refetchEvents();
|
||||
}
|
||||
|
||||
// Modal schließen
|
||||
if (window.closeEventModal) {
|
||||
window.closeEventModal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ignoreConflicts() {
|
||||
this.hideConflictModal();
|
||||
this.showWarning('Konflikte werden ignoriert. Bitte manuell prüfen.');
|
||||
}
|
||||
|
||||
acceptRecommendation() {
|
||||
if (this.currentRecommendation) {
|
||||
const printerSelect = document.getElementById('eventPrinter');
|
||||
if (printerSelect) {
|
||||
printerSelect.value = this.currentRecommendation.printer_id;
|
||||
this.dismissRecommendation();
|
||||
this.showSuccess('Empfehlung angenommen! 🎯');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dismissRecommendation() {
|
||||
const widget = document.getElementById('smartRecommendationWidget');
|
||||
if (widget) {
|
||||
widget.classList.add('hidden');
|
||||
}
|
||||
this.currentRecommendation = null;
|
||||
}
|
||||
|
||||
async refreshAvailability() {
|
||||
const now = new Date();
|
||||
const endTime = new Date(now.getTime() + 24 * 60 * 60 * 1000); // +24h
|
||||
|
||||
await this.loadPrinterAvailability(
|
||||
now.toISOString(),
|
||||
endTime.toISOString()
|
||||
);
|
||||
}
|
||||
|
||||
// =================== HELPER METHODS ===================
|
||||
|
||||
getFormData() {
|
||||
return {
|
||||
title: this.getFieldValue('eventTitle'),
|
||||
description: this.getFieldValue('eventDescription'),
|
||||
printer_id: this.getFieldValue('eventPrinter') || null,
|
||||
start_time: this.getFieldValue('eventStart'),
|
||||
end_time: this.getFieldValue('eventEnd'),
|
||||
priority: this.getFieldValue('eventPriority') || 'normal'
|
||||
};
|
||||
}
|
||||
|
||||
getFieldValue(fieldId) {
|
||||
const field = document.getElementById(fieldId);
|
||||
return field ? field.value : '';
|
||||
}
|
||||
|
||||
updateElement(elementId, content) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
element.textContent = content;
|
||||
}
|
||||
}
|
||||
|
||||
// =================== NOTIFICATIONS ===================
|
||||
|
||||
showSuccess(message) {
|
||||
this.showNotification(message, 'success');
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
this.showNotification(message, 'error');
|
||||
}
|
||||
|
||||
showWarning(message) {
|
||||
this.showNotification(message, 'warning');
|
||||
}
|
||||
|
||||
showNotification(message, type = 'info') {
|
||||
const colors = {
|
||||
success: 'from-green-500 to-green-600',
|
||||
error: 'from-red-500 to-red-600',
|
||||
warning: 'from-amber-500 to-amber-600',
|
||||
info: 'from-blue-500 to-blue-600'
|
||||
};
|
||||
|
||||
const icons = {
|
||||
success: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>',
|
||||
error: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>',
|
||||
warning: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 15.5c-.77.833.192 2.5 1.732 2.5z"/>',
|
||||
info: '<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"/>'
|
||||
};
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `fixed top-4 right-4 bg-gradient-to-r ${colors[type]} text-white px-6 py-4 rounded-lg shadow-xl z-50 transform transition-all duration-300 translate-x-full`;
|
||||
toast.innerHTML = `
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-6 h-6 bg-white/20 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
${icons[type]}
|
||||
</svg>
|
||||
</div>
|
||||
<span class="font-medium">${message}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
setTimeout(() => toast.classList.remove('translate-x-full'), 100);
|
||||
setTimeout(() => {
|
||||
toast.classList.add('translate-x-full');
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 4000);
|
||||
}
|
||||
}
|
||||
|
||||
// Globale Instanz für einfache Nutzung
|
||||
window.conflictManager = new ConflictManager();
|
||||
|
||||
// Integration mit bestehenden Kalender-Funktionen
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('✅ ConflictManager erfolgreich geladen');
|
||||
|
||||
// Integration mit bestehendem Formular
|
||||
const existingForm = document.getElementById('eventForm');
|
||||
if (existingForm) {
|
||||
// Existing form handler erweitern
|
||||
existingForm.addEventListener('submit', async function(e) {
|
||||
const formData = window.conflictManager.getFormData();
|
||||
|
||||
// Konflikte vor Submit prüfen
|
||||
const conflicts = await window.conflictManager.checkConflicts(formData);
|
||||
if (conflicts && conflicts.has_conflicts && !conflicts.can_proceed) {
|
||||
e.preventDefault();
|
||||
window.conflictManager.showConflictModal(conflicts);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
@ -818,6 +818,11 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"/>
|
||||
</svg>
|
||||
Produktionseinheit
|
||||
<button type="button" id="refreshAvailability"
|
||||
class="ml-2 text-blue-500 hover:text-blue-700 text-sm underline"
|
||||
onclick="window.conflictManager && window.conflictManager.refreshAvailability()">
|
||||
🔄 Verfügbarkeit prüfen
|
||||
</button>
|
||||
</label>
|
||||
<select id="printerFilter" class="mercedes-form-input block w-full px-4 py-3 rounded-lg">
|
||||
<option value="">Alle Produktionseinheiten</option>
|
||||
@ -1097,6 +1102,14 @@
|
||||
class="btn-secondary">
|
||||
Abbrechen
|
||||
</button>
|
||||
<button type="button" id="checkConflictsBtn"
|
||||
class="px-4 py-2 border border-blue-300 text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded-lg transition-colors flex items-center gap-2"
|
||||
onclick="window.conflictManager && window.conflictManager.checkConflicts(window.conflictManager.getFormData()).then(conflicts => { if (conflicts) { if (conflicts.has_conflicts) { window.conflictManager.showConflictModal(conflicts); } else { window.conflictManager.showSuccess('Keine Konflikte gefunden! ✅'); } } })">
|
||||
<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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
Konflikte prüfen
|
||||
</button>
|
||||
<button type="submit"
|
||||
class="btn-primary">
|
||||
Auftrag speichern
|
||||
@ -1118,6 +1131,9 @@
|
||||
<script src="{{ url_for('static', filename='js/fullcalendar/timegrid.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/fullcalendar/interaction.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/fullcalendar/list.min.js') }}"></script>
|
||||
|
||||
<!-- Erweiterte Konfliktmanagement-Engine -->
|
||||
<script src="{{ url_for('static', filename='js/conflict-manager.js') }}"></script>
|
||||
<input type="hidden" id="canEditFlag" value="{% if can_edit %}true{% else %}false{% endif %}">
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
BIN
backend/utils/__pycache__/conflict_manager.cpython-313.pyc
Normal file
BIN
backend/utils/__pycache__/conflict_manager.cpython-313.pyc
Normal file
Binary file not shown.
@ -18,8 +18,7 @@ from enum import Enum
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, or_
|
||||
|
||||
from models import Job, Printer, User
|
||||
from database.db_manager import get_cached_session
|
||||
from models import Job, Printer, User, get_cached_session
|
||||
|
||||
# Logging setup
|
||||
logger = logging.getLogger(__name__)
|
||||
|
Loading…
x
Reference in New Issue
Block a user