manage-your-printer/static/js/conflict-manager.min.js
2025-06-04 10:03:22 +02:00

1 line
20 KiB
JavaScript

class ConflictManager{constructor(){this.lastConflictCheck = null;this.currentRecommendation = null;this.autoCheckEnabled = true;this.checkTimeout = null;this.debounceDelay = 500;this.init();}init(){this.createConflictModal();this.createAvailabilityPanel();this.createSmartRecommendationWidget();this.attachEventListeners();console.log('🔧 ConflictManager initialisiert');}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>`;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>`;const calendarContainer = document.querySelector('.container');if(calendarContainer){calendarContainer.insertAdjacentHTML('afterbegin',widgetHTML);}}attachEventListeners(){document.getElementById('closeConflictModal')?.addEventListener('click',()=>{this.hideConflictModal();});document.getElementById('applyAutoFix')?.addEventListener('click',()=>{this.applyAutoFix();});document.getElementById('ignoreConflicts')?.addEventListener('click',()=>{this.ignoreConflicts();});document.getElementById('acceptRecommendation')?.addEventListener('click',()=>{this.acceptRecommendation();});document.getElementById('dismissRecommendation')?.addEventListener('click',()=>{this.dismissRecommendation();});this.attachFormValidation();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();});}});}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;}}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);});}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;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()}`);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');}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;const formHash = JSON.stringify(formData);if(this.lastFormHash === formHash)return;this.lastFormHash = formHash;try{const conflicts = await this.checkConflicts(formData);if(conflicts){this.showInlineConflictWarning(conflicts);}if(!formData.printer_id){await this.getSmartRecommendation(formData);}}catch(error){console.error('❌ Fehler bei Echzeit-Validierung:',error);}}showInlineConflictWarning(conflictData){const warning = document.getElementById('conflictWarning');if(!warning)return;if(conflictData.has_conflicts){warning.classList.remove('hidden');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');}}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! ✅');if(window.calendar){window.calendar.refetchEvents();}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);await this.loadPrinterAvailability(now.toISOString(),endTime.toISOString());}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;}}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);}}window.conflictManager = new ConflictManager();document.addEventListener('DOMContentLoaded',function(){console.log('✅ ConflictManager erfolgreich geladen');const existingForm = document.getElementById('eventForm');if(existingForm){existingForm.addEventListener('submit',async function(e){const formData = window.conflictManager.getFormData();const conflicts = await window.conflictManager.checkConflicts(formData);if(conflicts && conflicts.has_conflicts && !conflicts.can_proceed){e.preventDefault();window.conflictManager.showConflictModal(conflicts);return false;}});}});