📚 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 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': 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 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 - ❌ 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 - [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 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 - ✅ 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 - ✅ Globaler subprocess-Patch angewendet
|
||||||
2025-06-02 10:02:51 - [windows_fixes] windows_fixes - [INFO] INFO - ✅ Alle Windows-Fixes erfolgreich 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}')
|
local ram_mb=$(free -m | awk '/^Mem:/{print $2}')
|
||||||
progress "Verfügbarer RAM: ${ram_mb}MB"
|
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"
|
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)"
|
success "✅ Ausreichend RAM verfügbar (${ram_mb}MB)"
|
||||||
|
else
|
||||||
|
warning "⚠️ RAM-Größe konnte nicht ermittelt werden"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Festplattenplatz prüfen
|
# Festplattenplatz prüfen
|
||||||
local disk_free_gb=$(df / | awk 'NR==2{printf "%.1f", $4/1024/1024}')
|
local disk_free_gb=$(df / | awk 'NR==2{printf "%.1f", $4/1024/1024}')
|
||||||
progress "Verfügbarer Festplattenplatz: ${disk_free_gb}GB"
|
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)"
|
warning "⚠️ Wenig Festplattenplatz verfügbar (${disk_free_gb}GB)"
|
||||||
else
|
else
|
||||||
success "✅ Ausreichend Festplattenplatz verfügbar (${disk_free_gb}GB)"
|
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"/>
|
<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>
|
</svg>
|
||||||
Produktionseinheit
|
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>
|
</label>
|
||||||
<select id="printerFilter" class="mercedes-form-input block w-full px-4 py-3 rounded-lg">
|
<select id="printerFilter" class="mercedes-form-input block w-full px-4 py-3 rounded-lg">
|
||||||
<option value="">Alle Produktionseinheiten</option>
|
<option value="">Alle Produktionseinheiten</option>
|
||||||
@ -1097,6 +1102,14 @@
|
|||||||
class="btn-secondary">
|
class="btn-secondary">
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</button>
|
</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"
|
<button type="submit"
|
||||||
class="btn-primary">
|
class="btn-primary">
|
||||||
Auftrag speichern
|
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/timegrid.min.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/fullcalendar/interaction.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>
|
<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 %}">
|
<input type="hidden" id="canEditFlag" value="{% if can_edit %}true{% else %}false{% endif %}">
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
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.orm import Session
|
||||||
from sqlalchemy import and_, or_
|
from sqlalchemy import and_, or_
|
||||||
|
|
||||||
from models import Job, Printer, User
|
from models import Job, Printer, User, get_cached_session
|
||||||
from database.db_manager import get_cached_session
|
|
||||||
|
|
||||||
# Logging setup
|
# Logging setup
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user