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

741 lines
30 KiB
JavaScript

/**
* 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;
}
});
}
});