741 lines
30 KiB
JavaScript
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;
|
|
}
|
|
});
|
|
}
|
|
});
|