"feat: Implement optimization features in admin-guest requests
This commit is contained in:
parent
356fdf9b19
commit
3218328dd7
Binary file not shown.
Binary file not shown.
@ -1 +1,880 @@
|
||||
|
||||
/**
|
||||
* Mercedes-Benz MYP Admin Guest Requests Management
|
||||
* Moderne Verwaltung von Gastaufträgen mit Live-Updates
|
||||
*/
|
||||
|
||||
// Globale Variablen
|
||||
let currentRequests = [];
|
||||
let filteredRequests = [];
|
||||
let currentPage = 0;
|
||||
let totalPages = 0;
|
||||
let totalRequests = 0;
|
||||
let refreshInterval = null;
|
||||
let csrfToken = '';
|
||||
|
||||
// API Base URL Detection
|
||||
function detectApiBaseUrl() {
|
||||
const currentHost = window.location.hostname;
|
||||
const currentProtocol = window.location.protocol;
|
||||
const currentPort = window.location.port;
|
||||
|
||||
if (currentPort === '443' || !currentPort) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return `https://${currentHost}`;
|
||||
}
|
||||
|
||||
const API_BASE_URL = detectApiBaseUrl();
|
||||
|
||||
// Initialisierung beim Laden der Seite
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// CSRF Token abrufen
|
||||
csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
|
||||
|
||||
// Event Listeners initialisieren
|
||||
initEventListeners();
|
||||
|
||||
// Daten initial laden
|
||||
loadGuestRequests();
|
||||
|
||||
// Auto-Refresh starten
|
||||
startAutoRefresh();
|
||||
|
||||
console.log('🎯 Admin Guest Requests Management geladen');
|
||||
});
|
||||
|
||||
/**
|
||||
* Event Listeners initialisieren
|
||||
*/
|
||||
function initEventListeners() {
|
||||
// Search Input
|
||||
const searchInput = document.getElementById('search-requests');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', debounce(handleSearch, 300));
|
||||
}
|
||||
|
||||
// Status Filter
|
||||
const statusFilter = document.getElementById('status-filter');
|
||||
if (statusFilter) {
|
||||
statusFilter.addEventListener('change', handleFilterChange);
|
||||
}
|
||||
|
||||
// Sort Order
|
||||
const sortOrder = document.getElementById('sort-order');
|
||||
if (sortOrder) {
|
||||
sortOrder.addEventListener('change', handleSortChange);
|
||||
}
|
||||
|
||||
// Action Buttons
|
||||
const refreshBtn = document.getElementById('refresh-btn');
|
||||
if (refreshBtn) {
|
||||
refreshBtn.addEventListener('click', () => {
|
||||
loadGuestRequests();
|
||||
showNotification('🔄 Gastaufträge aktualisiert', 'info');
|
||||
});
|
||||
}
|
||||
|
||||
const exportBtn = document.getElementById('export-btn');
|
||||
if (exportBtn) {
|
||||
exportBtn.addEventListener('click', handleExport);
|
||||
}
|
||||
|
||||
const bulkActionsBtn = document.getElementById('bulk-actions-btn');
|
||||
if (bulkActionsBtn) {
|
||||
bulkActionsBtn.addEventListener('click', showBulkActionsModal);
|
||||
}
|
||||
|
||||
// Select All Checkbox
|
||||
const selectAllCheckbox = document.getElementById('select-all');
|
||||
if (selectAllCheckbox) {
|
||||
selectAllCheckbox.addEventListener('change', handleSelectAll);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gastaufträge von der API laden
|
||||
*/
|
||||
async function loadGuestRequests() {
|
||||
try {
|
||||
showLoading(true);
|
||||
|
||||
const url = `${API_BASE_URL}/api/admin/guest-requests`;
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
currentRequests = data.requests || [];
|
||||
totalRequests = data.total || 0;
|
||||
|
||||
// Statistiken aktualisieren
|
||||
updateStats(data.stats || {});
|
||||
|
||||
// Tabelle aktualisieren
|
||||
applyFiltersAndSort();
|
||||
|
||||
console.log(`✅ ${currentRequests.length} Gastaufträge geladen`);
|
||||
} else {
|
||||
throw new Error(data.message || 'Fehler beim Laden der Gastaufträge');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Gastaufträge:', error);
|
||||
showNotification('❌ Fehler beim Laden der Gastaufträge: ' + error.message, 'error');
|
||||
showEmptyState();
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistiken aktualisieren
|
||||
*/
|
||||
function updateStats(stats) {
|
||||
const elements = {
|
||||
'pending-count': stats.pending || 0,
|
||||
'approved-count': stats.approved || 0,
|
||||
'rejected-count': stats.rejected || 0,
|
||||
'total-count': stats.total || 0
|
||||
};
|
||||
|
||||
Object.entries(elements).forEach(([id, value]) => {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
animateCounter(element, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Counter mit Animation
|
||||
*/
|
||||
function animateCounter(element, targetValue) {
|
||||
const currentValue = parseInt(element.textContent) || 0;
|
||||
const difference = targetValue - currentValue;
|
||||
const steps = 20;
|
||||
const stepValue = difference / steps;
|
||||
|
||||
let step = 0;
|
||||
const interval = setInterval(() => {
|
||||
step++;
|
||||
const value = Math.round(currentValue + (stepValue * step));
|
||||
element.textContent = value;
|
||||
|
||||
if (step >= steps) {
|
||||
clearInterval(interval);
|
||||
element.textContent = targetValue;
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter und Sortierung anwenden
|
||||
*/
|
||||
function applyFiltersAndSort() {
|
||||
let requests = [...currentRequests];
|
||||
|
||||
// Status Filter
|
||||
const statusFilter = document.getElementById('status-filter')?.value;
|
||||
if (statusFilter && statusFilter !== 'all') {
|
||||
requests = requests.filter(req => req.status === statusFilter);
|
||||
}
|
||||
|
||||
// Such-Filter
|
||||
const searchTerm = document.getElementById('search-requests')?.value.toLowerCase();
|
||||
if (searchTerm) {
|
||||
requests = requests.filter(req =>
|
||||
req.name?.toLowerCase().includes(searchTerm) ||
|
||||
req.email?.toLowerCase().includes(searchTerm) ||
|
||||
req.file_name?.toLowerCase().includes(searchTerm) ||
|
||||
req.reason?.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
}
|
||||
|
||||
// Sortierung
|
||||
const sortOrder = document.getElementById('sort-order')?.value;
|
||||
requests.sort((a, b) => {
|
||||
switch (sortOrder) {
|
||||
case 'oldest':
|
||||
return new Date(a.created_at) - new Date(b.created_at);
|
||||
case 'priority':
|
||||
return getPriorityValue(b) - getPriorityValue(a);
|
||||
case 'newest':
|
||||
default:
|
||||
return new Date(b.created_at) - new Date(a.created_at);
|
||||
}
|
||||
});
|
||||
|
||||
filteredRequests = requests;
|
||||
renderRequestsTable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prioritätswert für Sortierung berechnen
|
||||
*/
|
||||
function getPriorityValue(request) {
|
||||
const now = new Date();
|
||||
const created = new Date(request.created_at);
|
||||
const hoursOld = (now - created) / (1000 * 60 * 60);
|
||||
|
||||
let priority = 0;
|
||||
|
||||
// Status-basierte Priorität
|
||||
if (request.status === 'pending') priority += 10;
|
||||
else if (request.status === 'approved') priority += 5;
|
||||
|
||||
// Alter-basierte Priorität
|
||||
if (hoursOld > 24) priority += 5;
|
||||
else if (hoursOld > 8) priority += 3;
|
||||
else if (hoursOld > 2) priority += 1;
|
||||
|
||||
return priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests-Tabelle rendern
|
||||
*/
|
||||
function renderRequestsTable() {
|
||||
const tableBody = document.getElementById('requests-table-body');
|
||||
const emptyState = document.getElementById('empty-state');
|
||||
|
||||
if (!tableBody) return;
|
||||
|
||||
if (filteredRequests.length === 0) {
|
||||
tableBody.innerHTML = '';
|
||||
showEmptyState();
|
||||
return;
|
||||
}
|
||||
|
||||
hideEmptyState();
|
||||
|
||||
const requestsHtml = filteredRequests.map(request => createRequestRow(request)).join('');
|
||||
tableBody.innerHTML = requestsHtml;
|
||||
|
||||
// Event Listeners für neue Rows hinzufügen
|
||||
addRowEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Request Row HTML erstellen
|
||||
*/
|
||||
function createRequestRow(request) {
|
||||
const statusColor = getStatusColor(request.status);
|
||||
const priorityLevel = getPriorityLevel(request);
|
||||
const timeAgo = getTimeAgo(request.created_at);
|
||||
|
||||
return `
|
||||
<tr class="hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors duration-200" data-request-id="${request.id}">
|
||||
<td class="px-6 py-4">
|
||||
<input type="checkbox" class="request-checkbox rounded border-slate-300 text-blue-600 focus:ring-blue-500"
|
||||
value="${request.id}">
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 h-10 w-10">
|
||||
<div class="h-10 w-10 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-white font-semibold">
|
||||
${request.name ? request.name[0].toUpperCase() : 'G'}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<div class="text-sm font-medium text-slate-900 dark:text-white">${escapeHtml(request.name || 'Unbekannt')}</div>
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400">${escapeHtml(request.email || 'Keine E-Mail')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm text-slate-900 dark:text-white font-medium">${escapeHtml(request.file_name || 'Keine Datei')}</div>
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400">
|
||||
${request.duration_minutes ? `${request.duration_minutes} Min.` : 'Unbekannte Dauer'}
|
||||
${request.copies ? ` • ${request.copies} Kopien` : ''}
|
||||
</div>
|
||||
${request.reason ? `<div class="text-xs text-slate-400 dark:text-slate-500 mt-1 truncate max-w-xs">${escapeHtml(request.reason)}</div>` : ''}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full ${statusColor}">
|
||||
<span class="w-2 h-2 mr-1 rounded-full ${getStatusDot(request.status)}"></span>
|
||||
${getStatusText(request.status)}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm text-slate-900 dark:text-white">${timeAgo}</div>
|
||||
<div class="text-xs text-slate-500 dark:text-slate-400">${formatDateTime(request.created_at)}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center">
|
||||
${getPriorityBadge(priorityLevel)}
|
||||
${request.is_urgent ? '<span class="ml-2 text-red-500 text-xs">🔥 Dringend</span>' : ''}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
<button onclick="showRequestDetail(${request.id})"
|
||||
class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300 transition-colors"
|
||||
title="Details anzeigen">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
${request.status === 'pending' ? `
|
||||
<button onclick="approveRequest(${request.id})"
|
||||
class="text-green-600 hover:text-green-900 dark:text-green-400 dark:hover:text-green-300 transition-colors"
|
||||
title="Genehmigen">
|
||||
<svg class="w-5 h-5" 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>
|
||||
</button>
|
||||
|
||||
<button onclick="rejectRequest(${request.id})"
|
||||
class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 transition-colors"
|
||||
title="Ablehnen">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</button>
|
||||
` : ''}
|
||||
|
||||
<button onclick="deleteRequest(${request.id})"
|
||||
class="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300 transition-colors"
|
||||
title="Löschen">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Status-Helper-Funktionen
|
||||
*/
|
||||
function getStatusColor(status) {
|
||||
const colors = {
|
||||
'pending': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
|
||||
'approved': 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
||||
'rejected': 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
|
||||
'expired': 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200'
|
||||
};
|
||||
return colors[status] || 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200';
|
||||
}
|
||||
|
||||
function getStatusDot(status) {
|
||||
const dots = {
|
||||
'pending': 'bg-yellow-400',
|
||||
'approved': 'bg-green-400',
|
||||
'rejected': 'bg-red-400',
|
||||
'expired': 'bg-gray-400'
|
||||
};
|
||||
return dots[status] || 'bg-gray-400';
|
||||
}
|
||||
|
||||
function getStatusText(status) {
|
||||
const texts = {
|
||||
'pending': 'Wartend',
|
||||
'approved': 'Genehmigt',
|
||||
'rejected': 'Abgelehnt',
|
||||
'expired': 'Abgelaufen'
|
||||
};
|
||||
return texts[status] || status;
|
||||
}
|
||||
|
||||
function getPriorityLevel(request) {
|
||||
const priority = getPriorityValue(request);
|
||||
if (priority >= 15) return 'high';
|
||||
if (priority >= 8) return 'medium';
|
||||
return 'low';
|
||||
}
|
||||
|
||||
function getPriorityBadge(level) {
|
||||
const badges = {
|
||||
'high': '<span class="inline-flex items-center px-2 py-1 text-xs font-medium rounded-full bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200">🔴 Hoch</span>',
|
||||
'medium': '<span class="inline-flex items-center px-2 py-1 text-xs font-medium rounded-full bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200">🟡 Mittel</span>',
|
||||
'low': '<span class="inline-flex items-center px-2 py-1 text-xs font-medium rounded-full bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">🟢 Niedrig</span>'
|
||||
};
|
||||
return badges[level] || badges['low'];
|
||||
}
|
||||
|
||||
/**
|
||||
* CRUD-Operationen
|
||||
*/
|
||||
async function approveRequest(requestId) {
|
||||
if (!confirm('Möchten Sie diesen Gastauftrag wirklich genehmigen?')) return;
|
||||
|
||||
try {
|
||||
showLoading(true);
|
||||
|
||||
const url = `${API_BASE_URL}/api/guest-requests/${requestId}/approve`;
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showNotification('✅ Gastauftrag erfolgreich genehmigt', 'success');
|
||||
loadGuestRequests();
|
||||
} else {
|
||||
throw new Error(data.message || 'Fehler beim Genehmigen');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Genehmigen:', error);
|
||||
showNotification('❌ Fehler beim Genehmigen: ' + error.message, 'error');
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function rejectRequest(requestId) {
|
||||
const reason = prompt('Grund für die Ablehnung:');
|
||||
if (!reason) return;
|
||||
|
||||
try {
|
||||
showLoading(true);
|
||||
|
||||
const url = `${API_BASE_URL}/api/guest-requests/${requestId}/reject`;
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify({ reason })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showNotification('✅ Gastauftrag erfolgreich abgelehnt', 'success');
|
||||
loadGuestRequests();
|
||||
} else {
|
||||
throw new Error(data.message || 'Fehler beim Ablehnen');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Ablehnen:', error);
|
||||
showNotification('❌ Fehler beim Ablehnen: ' + error.message, 'error');
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRequest(requestId) {
|
||||
if (!confirm('Möchten Sie diesen Gastauftrag wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.')) return;
|
||||
|
||||
try {
|
||||
showLoading(true);
|
||||
|
||||
const url = `${API_BASE_URL}/api/guest-requests/${requestId}`;
|
||||
const response = await fetch(url, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showNotification('✅ Gastauftrag erfolgreich gelöscht', 'success');
|
||||
loadGuestRequests();
|
||||
} else {
|
||||
throw new Error(data.message || 'Fehler beim Löschen');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen:', error);
|
||||
showNotification('❌ Fehler beim Löschen: ' + error.message, 'error');
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detail-Modal Funktionen
|
||||
*/
|
||||
function showRequestDetail(requestId) {
|
||||
const request = currentRequests.find(req => req.id === requestId);
|
||||
if (!request) return;
|
||||
|
||||
const modal = document.getElementById('detail-modal');
|
||||
const content = document.getElementById('modal-content');
|
||||
|
||||
content.innerHTML = `
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white">Gastauftrag Details</h3>
|
||||
<button onclick="closeDetailModal()" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-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>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-lg font-semibold text-gray-900 dark:text-white">Antragsteller</h4>
|
||||
<div class="bg-slate-50 dark:bg-slate-700 rounded-lg p-4">
|
||||
<p><strong>Name:</strong> ${escapeHtml(request.name || 'Unbekannt')}</p>
|
||||
<p><strong>E-Mail:</strong> ${escapeHtml(request.email || 'Keine E-Mail')}</p>
|
||||
<p><strong>Erstellt am:</strong> ${formatDateTime(request.created_at)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-lg font-semibold text-gray-900 dark:text-white">Auftrag Details</h4>
|
||||
<div class="bg-slate-50 dark:bg-slate-700 rounded-lg p-4">
|
||||
<p><strong>Datei:</strong> ${escapeHtml(request.file_name || 'Keine Datei')}</p>
|
||||
<p><strong>Dauer:</strong> ${request.duration_minutes || 'Unbekannt'} Minuten</p>
|
||||
<p><strong>Kopien:</strong> ${request.copies || 1}</p>
|
||||
<p><strong>Status:</strong> ${getStatusText(request.status)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${request.reason ? `
|
||||
<div class="mt-6">
|
||||
<h4 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Begründung</h4>
|
||||
<div class="bg-slate-50 dark:bg-slate-700 rounded-lg p-4">
|
||||
<p class="text-gray-700 dark:text-gray-300">${escapeHtml(request.reason)}</p>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="mt-8 flex justify-end space-x-3">
|
||||
${request.status === 'pending' ? `
|
||||
<button onclick="approveRequest(${request.id}); closeDetailModal();"
|
||||
class="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors">
|
||||
Genehmigen
|
||||
</button>
|
||||
<button onclick="rejectRequest(${request.id}); closeDetailModal();"
|
||||
class="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors">
|
||||
Ablehnen
|
||||
</button>
|
||||
` : ''}
|
||||
<button onclick="closeDetailModal()"
|
||||
class="px-4 py-2 bg-gray-300 dark:bg-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-400 dark:hover:bg-gray-500 transition-colors">
|
||||
Schließen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeDetailModal() {
|
||||
const modal = document.getElementById('detail-modal');
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk Actions
|
||||
*/
|
||||
function showBulkActionsModal() {
|
||||
const selectedIds = getSelectedRequestIds();
|
||||
if (selectedIds.length === 0) {
|
||||
showNotification('⚠️ Bitte wählen Sie mindestens einen Gastauftrag aus', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const modal = document.getElementById('bulk-modal');
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeBulkModal() {
|
||||
const modal = document.getElementById('bulk-modal');
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
|
||||
async function performBulkAction(action) {
|
||||
const selectedIds = getSelectedRequestIds();
|
||||
if (selectedIds.length === 0) return;
|
||||
|
||||
const confirmMessages = {
|
||||
'approve': `Möchten Sie ${selectedIds.length} Gastaufträge genehmigen?`,
|
||||
'reject': `Möchten Sie ${selectedIds.length} Gastaufträge ablehnen?`,
|
||||
'delete': `Möchten Sie ${selectedIds.length} Gastaufträge löschen?`
|
||||
};
|
||||
|
||||
if (!confirm(confirmMessages[action])) return;
|
||||
|
||||
try {
|
||||
showLoading(true);
|
||||
closeBulkModal();
|
||||
|
||||
const promises = selectedIds.map(async (id) => {
|
||||
const url = `${API_BASE_URL}/api/guest-requests/${id}/${action}`;
|
||||
const method = action === 'delete' ? 'DELETE' : 'POST';
|
||||
|
||||
return fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const results = await Promise.allSettled(promises);
|
||||
const successCount = results.filter(r => r.status === 'fulfilled' && r.value.ok).length;
|
||||
|
||||
showNotification(`✅ ${successCount} von ${selectedIds.length} Aktionen erfolgreich`, 'success');
|
||||
loadGuestRequests();
|
||||
|
||||
// Alle Checkboxen zurücksetzen
|
||||
document.getElementById('select-all').checked = false;
|
||||
document.querySelectorAll('.request-checkbox').forEach(cb => cb.checked = false);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler bei Bulk-Aktion:', error);
|
||||
showNotification('❌ Fehler bei der Bulk-Aktion: ' + error.message, 'error');
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function getSelectedRequestIds() {
|
||||
const checkboxes = document.querySelectorAll('.request-checkbox:checked');
|
||||
return Array.from(checkboxes).map(cb => parseInt(cb.value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Event Handlers
|
||||
*/
|
||||
function handleSearch() {
|
||||
applyFiltersAndSort();
|
||||
}
|
||||
|
||||
function handleFilterChange() {
|
||||
applyFiltersAndSort();
|
||||
}
|
||||
|
||||
function handleSortChange() {
|
||||
applyFiltersAndSort();
|
||||
}
|
||||
|
||||
function handleSelectAll(event) {
|
||||
const checkboxes = document.querySelectorAll('.request-checkbox');
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.checked = event.target.checked;
|
||||
});
|
||||
}
|
||||
|
||||
function handleExport() {
|
||||
const selectedIds = getSelectedRequestIds();
|
||||
const exportData = selectedIds.length > 0 ?
|
||||
filteredRequests.filter(req => selectedIds.includes(req.id)) :
|
||||
filteredRequests;
|
||||
|
||||
if (exportData.length === 0) {
|
||||
showNotification('⚠️ Keine Daten zum Exportieren verfügbar', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
exportToCSV(exportData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export-Funktionen
|
||||
*/
|
||||
function exportToCSV(data) {
|
||||
const headers = ['ID', 'Name', 'E-Mail', 'Datei', 'Status', 'Erstellt', 'Dauer (Min)', 'Kopien', 'Begründung'];
|
||||
const rows = data.map(req => [
|
||||
req.id,
|
||||
req.name || '',
|
||||
req.email || '',
|
||||
req.file_name || '',
|
||||
getStatusText(req.status),
|
||||
formatDateTime(req.created_at),
|
||||
req.duration_minutes || '',
|
||||
req.copies || '',
|
||||
req.reason || ''
|
||||
]);
|
||||
|
||||
const csvContent = [headers, ...rows]
|
||||
.map(row => row.map(field => `"${String(field).replace(/"/g, '""')}"`).join(','))
|
||||
.join('\n');
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
|
||||
if (link.download !== undefined) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', `gastauftraege_${new Date().toISOString().split('T')[0]}.csv`);
|
||||
link.style.visibility = 'hidden';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
showNotification('📄 CSV-Export erfolgreich erstellt', 'success');
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-Refresh
|
||||
*/
|
||||
function startAutoRefresh() {
|
||||
// Refresh alle 30 Sekunden
|
||||
refreshInterval = setInterval(() => {
|
||||
loadGuestRequests();
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
function stopAutoRefresh() {
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval);
|
||||
refreshInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility-Funktionen
|
||||
*/
|
||||
function addRowEventListeners() {
|
||||
// Falls notwendig, können hier zusätzliche Event Listener hinzugefügt werden
|
||||
}
|
||||
|
||||
function showLoading(show) {
|
||||
const loadingElement = document.getElementById('table-loading');
|
||||
const tableBody = document.getElementById('requests-table-body');
|
||||
|
||||
if (loadingElement) {
|
||||
loadingElement.classList.toggle('hidden', !show);
|
||||
}
|
||||
|
||||
if (show && tableBody) {
|
||||
tableBody.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
function showEmptyState() {
|
||||
const emptyState = document.getElementById('empty-state');
|
||||
if (emptyState) {
|
||||
emptyState.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function hideEmptyState() {
|
||||
const emptyState = document.getElementById('empty-state');
|
||||
if (emptyState) {
|
||||
emptyState.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function showNotification(message, type = 'info') {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `fixed top-4 right-4 px-6 py-4 rounded-xl shadow-2xl z-50 transform transition-all duration-500 translate-x-full ${
|
||||
type === 'success' ? 'bg-green-500 text-white' :
|
||||
type === 'error' ? 'bg-red-500 text-white' :
|
||||
type === 'warning' ? 'bg-yellow-500 text-black' :
|
||||
'bg-blue-500 text-white'
|
||||
}`;
|
||||
|
||||
notification.innerHTML = `
|
||||
<div class="flex items-center space-x-3">
|
||||
<span class="text-lg">
|
||||
${type === 'success' ? '✅' :
|
||||
type === 'error' ? '❌' :
|
||||
type === 'warning' ? '⚠️' : 'ℹ️'}
|
||||
</span>
|
||||
<span class="font-medium">${message}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Animation einblenden
|
||||
setTimeout(() => {
|
||||
notification.classList.remove('translate-x-full');
|
||||
}, 100);
|
||||
|
||||
// Nach 5 Sekunden entfernen
|
||||
setTimeout(() => {
|
||||
notification.classList.add('translate-x-full');
|
||||
setTimeout(() => notification.remove(), 5000);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const map = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
return text ? String(text).replace(/[&<>"']/g, m => map[m]) : '';
|
||||
}
|
||||
|
||||
function formatDateTime(dateString) {
|
||||
if (!dateString) return 'Unbekannt';
|
||||
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
function getTimeAgo(dateString) {
|
||||
if (!dateString) return 'Unbekannt';
|
||||
|
||||
const now = new Date();
|
||||
const date = new Date(dateString);
|
||||
const diffMs = now - date;
|
||||
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
|
||||
if (diffDays > 0) {
|
||||
return `vor ${diffDays} Tag${diffDays === 1 ? '' : 'en'}`;
|
||||
} else if (diffHours > 0) {
|
||||
return `vor ${diffHours} Stunde${diffHours === 1 ? '' : 'n'}`;
|
||||
} else {
|
||||
const diffMinutes = Math.floor(diffMs / (1000 * 60));
|
||||
return `vor ${Math.max(1, diffMinutes)} Minute${diffMinutes === 1 ? '' : 'n'}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Globale Funktionen für onclick-Handler
|
||||
window.showRequestDetail = showRequestDetail;
|
||||
window.approveRequest = approveRequest;
|
||||
window.rejectRequest = rejectRequest;
|
||||
window.deleteRequest = deleteRequest;
|
||||
window.closeDetailModal = closeDetailModal;
|
||||
window.closeBulkModal = closeBulkModal;
|
||||
window.performBulkAction = performBulkAction;
|
||||
|
||||
console.log('📋 Admin Guest Requests JavaScript vollständig geladen');
|
560
backend/app/static/js/optimization-features.js
Normal file
560
backend/app/static/js/optimization-features.js
Normal file
@ -0,0 +1,560 @@
|
||||
/**
|
||||
* MYP Platform - Erweiterte Optimierungs- und Batch-Funktionen
|
||||
* Implementiert Auto-Optimierung und Batch-Planung für 3D-Druckaufträge
|
||||
*/
|
||||
|
||||
class OptimizationManager {
|
||||
constructor() {
|
||||
this.isAutoOptimizationEnabled = false;
|
||||
this.isBatchModeEnabled = false;
|
||||
this.selectedJobs = new Set();
|
||||
this.optimizationSettings = {
|
||||
algorithm: 'round_robin', // 'round_robin', 'load_balance', 'priority_based'
|
||||
considerDistance: true,
|
||||
minimizeChangeover: true,
|
||||
maxBatchSize: 10,
|
||||
timeWindow: 24 // Stunden
|
||||
};
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupEventListeners();
|
||||
this.loadSavedSettings();
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Keyboard shortcuts
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.ctrlKey && e.altKey && e.key === 'O') {
|
||||
this.toggleAutoOptimization();
|
||||
e.preventDefault();
|
||||
}
|
||||
if (e.ctrlKey && e.altKey && e.key === 'B') {
|
||||
this.toggleBatchMode();
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-Optimierung ein-/ausschalten
|
||||
* Diese Funktion optimiert automatisch die Druckreihenfolge basierend auf verschiedenen Faktoren
|
||||
*/
|
||||
toggleAutoOptimization() {
|
||||
this.isAutoOptimizationEnabled = !this.isAutoOptimizationEnabled;
|
||||
|
||||
const button = document.getElementById('auto-opt-toggle');
|
||||
if (button) {
|
||||
this.updateAutoOptimizationButton(button);
|
||||
}
|
||||
|
||||
// Settings speichern
|
||||
localStorage.setItem('myp-auto-optimization', this.isAutoOptimizationEnabled);
|
||||
|
||||
// Notification anzeigen
|
||||
this.showOptimizationNotification(
|
||||
this.isAutoOptimizationEnabled ? 'aktiviert' : 'deaktiviert',
|
||||
'auto-optimization'
|
||||
);
|
||||
|
||||
// Wenn aktiviert, sofortige Optimierung durchführen
|
||||
if (this.isAutoOptimizationEnabled) {
|
||||
this.performAutoOptimization();
|
||||
}
|
||||
|
||||
// UI aktualisieren
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
updateAutoOptimizationButton(button) {
|
||||
const span = button.querySelector('span');
|
||||
const icon = button.querySelector('svg');
|
||||
|
||||
if (this.isAutoOptimizationEnabled) {
|
||||
button.classList.remove('btn-secondary');
|
||||
button.classList.add('btn-primary');
|
||||
span.textContent = 'Auto-Optimierung AN';
|
||||
|
||||
// Button-Animation
|
||||
button.style.transform = 'scale(1.05)';
|
||||
setTimeout(() => {
|
||||
button.style.transform = '';
|
||||
}, 200);
|
||||
|
||||
// Icon-Animation
|
||||
icon.style.animation = 'spin 1s ease-in-out';
|
||||
setTimeout(() => {
|
||||
icon.style.animation = '';
|
||||
}, 1000);
|
||||
} else {
|
||||
button.classList.remove('btn-primary');
|
||||
button.classList.add('btn-secondary');
|
||||
span.textContent = 'Auto-Optimierung';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch-Modus ein-/ausschalten
|
||||
* Ermöglicht die Auswahl mehrerer Jobs für Batch-Operationen
|
||||
*/
|
||||
toggleBatchMode() {
|
||||
this.isBatchModeEnabled = !this.isBatchModeEnabled;
|
||||
|
||||
const button = document.getElementById('batch-toggle');
|
||||
if (button) {
|
||||
this.updateBatchModeButton(button);
|
||||
}
|
||||
|
||||
// Batch-Funktionalität aktivieren/deaktivieren
|
||||
this.toggleBatchSelection();
|
||||
|
||||
// Settings speichern
|
||||
localStorage.setItem('myp-batch-mode', this.isBatchModeEnabled);
|
||||
|
||||
// Notification anzeigen
|
||||
this.showOptimizationNotification(
|
||||
this.isBatchModeEnabled ? 'aktiviert' : 'deaktiviert',
|
||||
'batch-mode'
|
||||
);
|
||||
|
||||
// UI aktualisieren
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
updateBatchModeButton(button) {
|
||||
const span = button.querySelector('span');
|
||||
|
||||
if (this.isBatchModeEnabled) {
|
||||
button.classList.remove('btn-secondary');
|
||||
button.classList.add('btn-warning');
|
||||
span.textContent = `Batch-Modus (${this.selectedJobs.size})`;
|
||||
|
||||
// Button-Animation
|
||||
button.style.transform = 'scale(1.05)';
|
||||
setTimeout(() => {
|
||||
button.style.transform = '';
|
||||
}, 200);
|
||||
} else {
|
||||
button.classList.remove('btn-warning');
|
||||
button.classList.add('btn-secondary');
|
||||
span.textContent = 'Mehrfachauswahl';
|
||||
|
||||
// Auswahl zurücksetzen
|
||||
this.selectedJobs.clear();
|
||||
}
|
||||
}
|
||||
|
||||
toggleBatchSelection() {
|
||||
const jobCards = document.querySelectorAll('.job-card, [data-job-id]');
|
||||
|
||||
jobCards.forEach(card => {
|
||||
if (this.isBatchModeEnabled) {
|
||||
this.enableBatchSelection(card);
|
||||
} else {
|
||||
this.disableBatchSelection(card);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
enableBatchSelection(card) {
|
||||
// Checkbox hinzufügen
|
||||
let checkbox = card.querySelector('.batch-checkbox');
|
||||
if (!checkbox) {
|
||||
checkbox = document.createElement('input');
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.className = 'batch-checkbox absolute top-3 left-3 w-5 h-5 rounded border-2 border-gray-300 text-blue-600 focus:ring-blue-500';
|
||||
checkbox.style.zIndex = '10';
|
||||
|
||||
// Event Listener für Checkbox
|
||||
checkbox.addEventListener('change', (e) => {
|
||||
const jobId = card.dataset.jobId;
|
||||
if (e.target.checked) {
|
||||
this.selectedJobs.add(jobId);
|
||||
card.classList.add('selected-for-batch');
|
||||
} else {
|
||||
this.selectedJobs.delete(jobId);
|
||||
card.classList.remove('selected-for-batch');
|
||||
}
|
||||
this.updateBatchCounter();
|
||||
});
|
||||
|
||||
// Checkbox in die Karte einfügen
|
||||
card.style.position = 'relative';
|
||||
card.appendChild(checkbox);
|
||||
}
|
||||
|
||||
checkbox.style.display = 'block';
|
||||
card.classList.add('batch-selectable');
|
||||
}
|
||||
|
||||
disableBatchSelection(card) {
|
||||
const checkbox = card.querySelector('.batch-checkbox');
|
||||
if (checkbox) {
|
||||
checkbox.style.display = 'none';
|
||||
}
|
||||
card.classList.remove('batch-selectable', 'selected-for-batch');
|
||||
}
|
||||
|
||||
updateBatchCounter() {
|
||||
const button = document.getElementById('batch-toggle');
|
||||
if (button && this.isBatchModeEnabled) {
|
||||
const span = button.querySelector('span');
|
||||
span.textContent = `Batch-Modus (${this.selectedJobs.size})`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatische Optimierung durchführen
|
||||
*/
|
||||
async performAutoOptimization() {
|
||||
try {
|
||||
const response = await fetch('/api/optimization/auto-optimize', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': this.getCSRFToken()
|
||||
},
|
||||
body: JSON.stringify({
|
||||
settings: this.optimizationSettings,
|
||||
enabled: this.isAutoOptimizationEnabled
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.showSuccessMessage(`Optimierung erfolgreich: ${data.optimized_jobs} Jobs optimiert`);
|
||||
this.refreshCurrentView();
|
||||
} else {
|
||||
this.showErrorMessage(`Optimierung fehlgeschlagen: ${data.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auto-Optimierung Fehler:', error);
|
||||
this.showErrorMessage('Netzwerkfehler bei der Auto-Optimierung');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch-Operationen durchführen
|
||||
*/
|
||||
async performBatchOperation(operation) {
|
||||
if (this.selectedJobs.size === 0) {
|
||||
this.showWarningMessage('Keine Jobs für Batch-Operation ausgewählt');
|
||||
return;
|
||||
}
|
||||
|
||||
const jobIds = Array.from(this.selectedJobs);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/jobs/batch-operation', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': this.getCSRFToken()
|
||||
},
|
||||
body: JSON.stringify({
|
||||
job_ids: jobIds,
|
||||
operation: operation
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.showSuccessMessage(`Batch-Operation "${operation}" erfolgreich auf ${jobIds.length} Jobs angewendet`);
|
||||
this.selectedJobs.clear();
|
||||
this.updateBatchCounter();
|
||||
this.refreshCurrentView();
|
||||
} else {
|
||||
this.showErrorMessage(`Batch-Operation fehlgeschlagen: ${data.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Batch-Operation Fehler:', error);
|
||||
this.showErrorMessage('Netzwerkfehler bei der Batch-Operation');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimierungs-Einstellungen konfigurieren
|
||||
*/
|
||||
showOptimizationSettings() {
|
||||
this.createOptimizationModal();
|
||||
}
|
||||
|
||||
createOptimizationModal() {
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'optimization-settings-modal';
|
||||
modal.className = 'fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4';
|
||||
|
||||
modal.innerHTML = `
|
||||
<div class="dashboard-card max-w-2xl w-full p-8 transform transition-all duration-300">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h3 class="text-xl font-bold text-slate-900 dark:text-white mb-2">
|
||||
Optimierungs-Einstellungen
|
||||
</h3>
|
||||
<p class="text-slate-500 dark:text-slate-400">
|
||||
Konfigurieren Sie die automatische Optimierung für maximale Effizienz
|
||||
</p>
|
||||
</div>
|
||||
<button onclick="this.closest('#optimization-settings-modal').remove()"
|
||||
class="p-2 hover:bg-gray-100 dark:hover:bg-slate-700 rounded-lg transition-colors">
|
||||
<svg class="w-6 h-6 text-slate-500 dark:text-slate-400" 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"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Optimierungs-Algorithmus
|
||||
</label>
|
||||
<select id="optimization-algorithm" class="block w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white">
|
||||
<option value="round_robin">Round Robin - Gleichmäßige Verteilung</option>
|
||||
<option value="load_balance">Load Balancing - Auslastungsoptimierung</option>
|
||||
<option value="priority_based">Prioritätsbasiert - Wichtige Jobs zuerst</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" id="consider-distance" class="mr-2">
|
||||
<span class="text-sm text-slate-700 dark:text-slate-300">
|
||||
Druckerentfernung berücksichtigen
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" id="minimize-changeover" class="mr-2">
|
||||
<span class="text-sm text-slate-700 dark:text-slate-300">
|
||||
Rüstzeiten minimieren
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Max. Batch-Größe
|
||||
</label>
|
||||
<input type="number" id="max-batch-size" min="1" max="50"
|
||||
class="block w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Planungshorizont (Stunden)
|
||||
</label>
|
||||
<input type="number" id="time-window" min="1" max="168"
|
||||
class="block w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-3 pt-6 border-t border-gray-200 dark:border-slate-600 mt-6">
|
||||
<button onclick="this.closest('#optimization-settings-modal').remove()"
|
||||
class="btn-secondary">
|
||||
Abbrechen
|
||||
</button>
|
||||
<button onclick="optimizationManager.saveOptimizationSettings()"
|
||||
class="btn-primary">
|
||||
Einstellungen speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
this.loadOptimizationSettingsInModal();
|
||||
}
|
||||
|
||||
loadOptimizationSettingsInModal() {
|
||||
document.getElementById('optimization-algorithm').value = this.optimizationSettings.algorithm;
|
||||
document.getElementById('consider-distance').checked = this.optimizationSettings.considerDistance;
|
||||
document.getElementById('minimize-changeover').checked = this.optimizationSettings.minimizeChangeover;
|
||||
document.getElementById('max-batch-size').value = this.optimizationSettings.maxBatchSize;
|
||||
document.getElementById('time-window').value = this.optimizationSettings.timeWindow;
|
||||
}
|
||||
|
||||
saveOptimizationSettings() {
|
||||
this.optimizationSettings.algorithm = document.getElementById('optimization-algorithm').value;
|
||||
this.optimizationSettings.considerDistance = document.getElementById('consider-distance').checked;
|
||||
this.optimizationSettings.minimizeChangeover = document.getElementById('minimize-changeover').checked;
|
||||
this.optimizationSettings.maxBatchSize = parseInt(document.getElementById('max-batch-size').value);
|
||||
this.optimizationSettings.timeWindow = parseInt(document.getElementById('time-window').value);
|
||||
|
||||
localStorage.setItem('myp-optimization-settings', JSON.stringify(this.optimizationSettings));
|
||||
|
||||
document.getElementById('optimization-settings-modal').remove();
|
||||
this.showSuccessMessage('Optimierungs-Einstellungen gespeichert');
|
||||
|
||||
// Wenn Auto-Optimierung aktiv ist, neue Optimierung durchführen
|
||||
if (this.isAutoOptimizationEnabled) {
|
||||
this.performAutoOptimization();
|
||||
}
|
||||
}
|
||||
|
||||
loadSavedSettings() {
|
||||
// Auto-Optimierung Status laden
|
||||
const savedAutoOpt = localStorage.getItem('myp-auto-optimization');
|
||||
if (savedAutoOpt !== null) {
|
||||
this.isAutoOptimizationEnabled = savedAutoOpt === 'true';
|
||||
}
|
||||
|
||||
// Batch-Modus Status laden
|
||||
const savedBatchMode = localStorage.getItem('myp-batch-mode');
|
||||
if (savedBatchMode !== null) {
|
||||
this.isBatchModeEnabled = savedBatchMode === 'true';
|
||||
}
|
||||
|
||||
// Optimierungs-Einstellungen laden
|
||||
const savedSettings = localStorage.getItem('myp-optimization-settings');
|
||||
if (savedSettings) {
|
||||
try {
|
||||
this.optimizationSettings = { ...this.optimizationSettings, ...JSON.parse(savedSettings) };
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Optimierungs-Einstellungen:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateUI() {
|
||||
// Buttons aktualisieren
|
||||
const autoOptButton = document.getElementById('auto-opt-toggle');
|
||||
if (autoOptButton) {
|
||||
this.updateAutoOptimizationButton(autoOptButton);
|
||||
}
|
||||
|
||||
const batchButton = document.getElementById('batch-toggle');
|
||||
if (batchButton) {
|
||||
this.updateBatchModeButton(batchButton);
|
||||
}
|
||||
|
||||
// Batch-Auswahl aktualisieren
|
||||
if (this.isBatchModeEnabled) {
|
||||
this.toggleBatchSelection();
|
||||
}
|
||||
}
|
||||
|
||||
// Utility-Funktionen
|
||||
getCSRFToken() {
|
||||
const token = document.querySelector('meta[name="csrf-token"]');
|
||||
return token ? token.getAttribute('content') : '';
|
||||
}
|
||||
|
||||
refreshCurrentView() {
|
||||
// Je nach aktueller Seite entsprechende Refresh-Funktion aufrufen
|
||||
if (typeof refreshJobs === 'function') {
|
||||
refreshJobs();
|
||||
} else if (typeof refreshCalendar === 'function') {
|
||||
refreshCalendar();
|
||||
} else if (typeof refreshDashboard === 'function') {
|
||||
refreshDashboard();
|
||||
}
|
||||
}
|
||||
|
||||
showOptimizationNotification(status, type) {
|
||||
const messages = {
|
||||
'auto-optimization': {
|
||||
'aktiviert': '🚀 Auto-Optimierung aktiviert - Jobs werden automatisch optimiert',
|
||||
'deaktiviert': '⏸️ Auto-Optimierung deaktiviert'
|
||||
},
|
||||
'batch-mode': {
|
||||
'aktiviert': '📦 Batch-Modus aktiviert - Wählen Sie Jobs für Batch-Operationen aus',
|
||||
'deaktiviert': '✅ Batch-Modus deaktiviert'
|
||||
}
|
||||
};
|
||||
|
||||
const message = messages[type]?.[status] || `${type} ${status}`;
|
||||
this.showSuccessMessage(message);
|
||||
}
|
||||
|
||||
showSuccessMessage(message) {
|
||||
this.showToast(message, 'success');
|
||||
}
|
||||
|
||||
showErrorMessage(message) {
|
||||
this.showToast(message, 'error');
|
||||
}
|
||||
|
||||
showWarningMessage(message) {
|
||||
this.showToast(message, 'warning');
|
||||
}
|
||||
|
||||
showToast(message, type = 'info') {
|
||||
// Einfache Toast-Benachrichtigung
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg transition-all duration-300 transform translate-x-full`;
|
||||
|
||||
const colors = {
|
||||
success: 'bg-green-500 text-white',
|
||||
error: 'bg-red-500 text-white',
|
||||
warning: 'bg-yellow-500 text-black',
|
||||
info: 'bg-blue-500 text-white'
|
||||
};
|
||||
|
||||
toast.className += ` ${colors[type]}`;
|
||||
toast.textContent = message;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// Animation einblenden
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('translate-x-full');
|
||||
}, 100);
|
||||
|
||||
// Nach 5 Sekunden automatisch entfernen
|
||||
setTimeout(() => {
|
||||
toast.classList.add('translate-x-full');
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 300);
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// Globale Funktionen für Template-Integration
|
||||
let optimizationManager;
|
||||
|
||||
// Auto-Optimierung umschalten
|
||||
window.toggleAutoOptimization = function() {
|
||||
if (!optimizationManager) {
|
||||
optimizationManager = new OptimizationManager();
|
||||
}
|
||||
optimizationManager.toggleAutoOptimization();
|
||||
};
|
||||
|
||||
// Batch-Modus umschalten
|
||||
window.toggleBatchMode = function() {
|
||||
if (!optimizationManager) {
|
||||
optimizationManager = new OptimizationManager();
|
||||
}
|
||||
optimizationManager.toggleBatchMode();
|
||||
};
|
||||
|
||||
// Batch-Planung Modal öffnen
|
||||
window.openBatchPlanningModal = function() {
|
||||
if (!optimizationManager) {
|
||||
optimizationManager = new OptimizationManager();
|
||||
}
|
||||
optimizationManager.showOptimizationSettings();
|
||||
};
|
||||
|
||||
// Optimierungs-Einstellungen anzeigen
|
||||
window.showOptimizationSettings = function() {
|
||||
if (!optimizationManager) {
|
||||
optimizationManager = new OptimizationManager();
|
||||
}
|
||||
optimizationManager.showOptimizationSettings();
|
||||
};
|
||||
|
||||
// Initialisierung beim Laden der Seite
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
optimizationManager = new OptimizationManager();
|
||||
console.log('🎯 Optimierungs-Manager initialisiert');
|
||||
});
|
@ -3,80 +3,354 @@
|
||||
{% block title %}Nutzungsbedingungen - MYP Platform{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-slate-900 dark:text-white transition-colors duration-300">Nutzungsbedingungen</h1>
|
||||
<p class="mt-2 text-slate-600 dark:text-slate-400 transition-colors duration-300">Gültig ab 15. Juni 2024</p>
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Hero Header mit Gradient und Animation -->
|
||||
<div class="relative overflow-hidden rounded-3xl mb-12 p-8 md:p-12 bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50 dark:from-slate-900 dark:via-blue-900/20 dark:to-indigo-900/20 border border-blue-200/50 dark:border-blue-700/30">
|
||||
<!-- Animated Background Pattern -->
|
||||
<div class="absolute inset-0 opacity-10 dark:opacity-5">
|
||||
<div class="absolute top-0 left-0 w-full h-full">
|
||||
<svg class="animate-pulse" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<defs>
|
||||
<pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse">
|
||||
<path d="M 10 0 L 0 0 0 10" fill="none" stroke="currentColor" stroke-width="0.5"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100" height="100" fill="url(#grid)" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="relative z-10 text-center">
|
||||
<!-- Icon -->
|
||||
<div class="inline-flex items-center justify-center w-20 h-20 rounded-full bg-blue-100 dark:bg-blue-900/50 mb-6">
|
||||
<svg class="w-10 h-10 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="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h1 class="text-4xl md:text-5xl font-bold text-slate-900 dark:text-white mb-4 tracking-tight">
|
||||
Nutzungsbedingungen
|
||||
</h1>
|
||||
<p class="text-xl text-slate-600 dark:text-slate-300 mb-6 max-w-2xl mx-auto">
|
||||
Rechtliche Grundlagen für die Nutzung der MYP Platform
|
||||
</p>
|
||||
|
||||
<!-- Meta Information -->
|
||||
<div class="inline-flex items-center gap-4 text-sm text-slate-500 dark:text-slate-400">
|
||||
<div class="flex items-center gap-2">
|
||||
<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="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
<span>Gültig ab 15. Juni 2024</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<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="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<span>Lesezeit: ~5 Minuten</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="glass-card mb-8">
|
||||
<div class="prose dark:prose-invert max-w-none">
|
||||
<h2>1. Allgemeines</h2>
|
||||
<p>Diese Nutzungsbedingungen regeln die Nutzung der Mercedes-Benz "Manage Your Printers" (MYP) Plattform. Durch die Nutzung der Plattform erklären Sie sich mit diesen Bedingungen einverstanden.</p>
|
||||
|
||||
<h2>2. Zugang und Nutzungsberechtigung</h2>
|
||||
<p>Die MYP-Plattform steht ausschließlich Mitarbeitern der Mercedes-Benz Group AG und ihren verbundenen Unternehmen zur Verfügung. Der Zugang erfolgt über eine persönliche Benutzerkennung und ein Passwort, die nicht an Dritte weitergegeben werden dürfen.</p>
|
||||
|
||||
<h2>3. Nutzungszweck</h2>
|
||||
<p>Die MYP-Plattform dient ausschließlich der Verwaltung und Überwachung von 3D-Druckaufträgen im Rahmen der beruflichen Tätigkeit. Eine private Nutzung ist nicht gestattet.</p>
|
||||
|
||||
<h2>4. Verantwortlichkeiten der Nutzer</h2>
|
||||
<p>Als Nutzer sind Sie verantwortlich für:</p>
|
||||
<ul>
|
||||
<li>Die Geheimhaltung Ihrer Zugangsdaten</li>
|
||||
<li>Die ordnungsgemäße Nutzung der Geräte und Ressourcen</li>
|
||||
<li>Die Einhaltung der Unternehmensrichtlinien zum Umgang mit 3D-Druckern</li>
|
||||
<li>Die Beachtung von Urheberrechten und Schutzrechten Dritter bei der Erstellung von 3D-Modellen</li>
|
||||
</ul>
|
||||
|
||||
<h2>5. Einschränkungen</h2>
|
||||
<p>Es ist untersagt:</p>
|
||||
<ul>
|
||||
<li>Die Plattform für nicht-geschäftliche Zwecke zu nutzen</li>
|
||||
<li>Unbefugten Zugang zu verschaffen</li>
|
||||
<li>Die Sicherheitsmaßnahmen der Plattform zu umgehen</li>
|
||||
<li>Schädlichen Code oder Malware hochzuladen</li>
|
||||
<li>Die Plattform zu überlasten oder ihre normale Funktion zu stören</li>
|
||||
</ul>
|
||||
|
||||
<h2>6. Verfügbarkeit und Wartung</h2>
|
||||
<p>Mercedes-Benz bemüht sich um eine hohe Verfügbarkeit der MYP-Plattform, kann jedoch keine ununterbrochene Verfügbarkeit garantieren. Wartungsarbeiten werden nach Möglichkeit im Voraus angekündigt.</p>
|
||||
|
||||
<h2>7. Haftung</h2>
|
||||
<p>Mercedes-Benz übernimmt keine Haftung für:</p>
|
||||
<ul>
|
||||
<li>Schäden, die durch fehlerhafte Druckaufträge entstehen</li>
|
||||
<li>Verlust von Daten oder Modellen</li>
|
||||
<li>Ausfallzeiten der Plattform</li>
|
||||
<li>Schäden durch unsachgemäße Verwendung der Drucker</li>
|
||||
</ul>
|
||||
|
||||
<h2>8. Datenschutz</h2>
|
||||
<p>Die Erhebung und Verarbeitung personenbezogener Daten erfolgt gemäß der <a href="/privacy" class="text-blue-600 dark:text-blue-400 hover:underline">Datenschutzerklärung</a>. Die Daten werden ausschließlich zur Verwaltung und Optimierung der Druckaufträge verwendet.</p>
|
||||
|
||||
<h2>9. Änderungen der Nutzungsbedingungen</h2>
|
||||
<p>Mercedes-Benz behält sich das Recht vor, diese Nutzungsbedingungen jederzeit zu ändern. Wesentliche Änderungen werden den Nutzern mitgeteilt.</p>
|
||||
|
||||
<h2>10. Beendigung der Nutzung</h2>
|
||||
<p>Die Nutzungsberechtigung endet automatisch mit dem Ausscheiden aus dem Unternehmen. Mercedes-Benz behält sich das Recht vor, den Zugang bei Verstößen gegen diese Nutzungsbedingungen zu sperren.</p>
|
||||
|
||||
<h2>11. Anwendbares Recht</h2>
|
||||
<p>Es gilt das Recht der Bundesrepublik Deutschland unter Ausschluss des UN-Kaufrechts.</p>
|
||||
|
||||
<h2>12. Kontakt</h2>
|
||||
<p>Bei Fragen zu diesen Nutzungsbedingungen wenden Sie sich bitte an:</p>
|
||||
<p>
|
||||
<a href="mailto:till.tomczak@mercedes-benz.com" class="text-blue-600 dark:text-blue-400 hover:underline">till.tomczak@mercedes-benz.com</a><br>
|
||||
<a href="mailto:torben.haack@mercedes-benz.com" class="text-blue-600 dark:text-blue-400 hover:underline">torben.haack@mercedes-benz.com</a>
|
||||
</p>
|
||||
<!-- Table of Contents -->
|
||||
<div class="mb-12">
|
||||
<div class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-6">
|
||||
<h2 class="text-lg font-semibold text-slate-900 dark:text-white mb-4 flex items-center gap-3">
|
||||
<svg class="w-5 h-5 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="M4 6h16M4 12h8m-8 6h16"/>
|
||||
</svg>
|
||||
Inhaltsverzeichnis
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
<a href="#section-1" class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-100/50 dark:hover:bg-slate-800/50 text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-all duration-200 group">
|
||||
<span class="w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400 text-xs font-semibold flex items-center justify-center group-hover:scale-110 transition-transform">1</span>
|
||||
<span class="text-sm">Allgemeines</span>
|
||||
</a>
|
||||
<a href="#section-2" class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-100/50 dark:hover:bg-slate-800/50 text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-all duration-200 group">
|
||||
<span class="w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400 text-xs font-semibold flex items-center justify-center group-hover:scale-110 transition-transform">2</span>
|
||||
<span class="text-sm">Zugang</span>
|
||||
</a>
|
||||
<a href="#section-3" class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-100/50 dark:hover:bg-slate-800/50 text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-all duration-200 group">
|
||||
<span class="w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400 text-xs font-semibold flex items-center justify-center group-hover:scale-110 transition-transform">3</span>
|
||||
<span class="text-sm">Nutzungszweck</span>
|
||||
</a>
|
||||
<a href="#section-4" class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-100/50 dark:hover:bg-slate-800/50 text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-all duration-200 group">
|
||||
<span class="w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400 text-xs font-semibold flex items-center justify-center group-hover:scale-110 transition-transform">4</span>
|
||||
<span class="text-sm">Verantwortlichkeiten</span>
|
||||
</a>
|
||||
<a href="#section-5" class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-100/50 dark:hover:bg-slate-800/50 text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-all duration-200 group">
|
||||
<span class="w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400 text-xs font-semibold flex items-center justify-center group-hover:scale-110 transition-transform">5</span>
|
||||
<span class="text-sm">Einschränkungen</span>
|
||||
</a>
|
||||
<a href="#section-6" class="flex items-center gap-2 p-3 rounded-lg hover:bg-slate-100/50 dark:hover:bg-slate-800/50 text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-all duration-200 group">
|
||||
<span class="w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400 text-xs font-semibold flex items-center justify-center group-hover:scale-110 transition-transform">6</span>
|
||||
<span class="text-sm">Verfügbarkeit</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Terms Content -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
|
||||
<!-- Sidebar Navigation (hidden on mobile) -->
|
||||
<div class="hidden lg:block">
|
||||
<div class="sticky top-8">
|
||||
<div class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-6">
|
||||
<h3 class="text-sm font-semibold text-slate-900 dark:text-white mb-4 uppercase tracking-wider">Navigation</h3>
|
||||
<nav class="space-y-2">
|
||||
<a href="#section-1" class="block text-sm text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 py-2 border-l-2 border-transparent hover:border-blue-600 dark:hover:border-blue-400 pl-3 transition-all duration-200">1. Allgemeines</a>
|
||||
<a href="#section-2" class="block text-sm text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 py-2 border-l-2 border-transparent hover:border-blue-600 dark:hover:border-blue-400 pl-3 transition-all duration-200">2. Zugang und Berechtigung</a>
|
||||
<a href="#section-3" class="block text-sm text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 py-2 border-l-2 border-transparent hover:border-blue-600 dark:hover:border-blue-400 pl-3 transition-all duration-200">3. Nutzungszweck</a>
|
||||
<a href="#section-4" class="block text-sm text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 py-2 border-l-2 border-transparent hover:border-blue-600 dark:hover:border-blue-400 pl-3 transition-all duration-200">4. Verantwortlichkeiten</a>
|
||||
<a href="#section-5" class="block text-sm text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 py-2 border-l-2 border-transparent hover:border-blue-600 dark:hover:border-blue-400 pl-3 transition-all duration-200">5. Einschränkungen</a>
|
||||
<a href="#section-6" class="block text-sm text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 py-2 border-l-2 border-transparent hover:border-blue-600 dark:hover:border-blue-400 pl-3 transition-all duration-200">6. Verfügbarkeit</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="lg:col-span-3">
|
||||
<div class="space-y-8">
|
||||
<!-- Section 1 -->
|
||||
<section id="section-1" class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-8 scroll-mt-8">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="w-10 h-10 rounded-full bg-blue-100 dark:bg-blue-900/50 flex items-center justify-center">
|
||||
<span class="text-blue-600 dark:text-blue-400 font-bold">1</span>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Allgemeines</h2>
|
||||
</div>
|
||||
<div class="prose dark:prose-invert max-w-none">
|
||||
<p class="text-slate-700 dark:text-slate-300 leading-relaxed">
|
||||
Diese Nutzungsbedingungen regeln die Nutzung der Mercedes-Benz "Manage Your Printers" (MYP) Plattform. Durch die Nutzung der Plattform erklären Sie sich mit diesen Bedingungen einverstanden.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section 2 -->
|
||||
<section id="section-2" class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-8 scroll-mt-8">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="w-10 h-10 rounded-full bg-green-100 dark:bg-green-900/50 flex items-center justify-center">
|
||||
<span class="text-green-600 dark:text-green-400 font-bold">2</span>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Zugang und Nutzungsberechtigung</h2>
|
||||
</div>
|
||||
<div class="prose dark:prose-invert max-w-none">
|
||||
<p class="text-slate-700 dark:text-slate-300 leading-relaxed">
|
||||
Die MYP-Plattform steht ausschließlich Mitarbeitern der Mercedes-Benz Group AG und ihren verbundenen Unternehmen zur Verfügung. Der Zugang erfolgt über eine persönliche Benutzerkennung und ein Passwort, die nicht an Dritte weitergegeben werden dürfen.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section 3 -->
|
||||
<section id="section-3" class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-8 scroll-mt-8">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="w-10 h-10 rounded-full bg-purple-100 dark:bg-purple-900/50 flex items-center justify-center">
|
||||
<span class="text-purple-600 dark:text-purple-400 font-bold">3</span>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Nutzungszweck</h2>
|
||||
</div>
|
||||
<div class="prose dark:prose-invert max-w-none">
|
||||
<p class="text-slate-700 dark:text-slate-300 leading-relaxed">
|
||||
Die MYP-Plattform dient ausschließlich der Verwaltung und Überwachung von 3D-Druckaufträgen im Rahmen der beruflichen Tätigkeit. Eine private Nutzung ist nicht gestattet.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section 4 -->
|
||||
<section id="section-4" class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-8 scroll-mt-8">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="w-10 h-10 rounded-full bg-orange-100 dark:bg-orange-900/50 flex items-center justify-center">
|
||||
<span class="text-orange-600 dark:text-orange-400 font-bold">4</span>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Verantwortlichkeiten der Nutzer</h2>
|
||||
</div>
|
||||
<div class="prose dark:prose-invert max-w-none">
|
||||
<p class="text-slate-700 dark:text-slate-300 leading-relaxed mb-4">Als Nutzer sind Sie verantwortlich für:</p>
|
||||
<div class="grid gap-3">
|
||||
<div class="flex items-start gap-3 p-4 rounded-lg bg-slate-50/50 dark:bg-slate-800/50">
|
||||
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
<span class="text-slate-700 dark:text-slate-300">Die Geheimhaltung Ihrer Zugangsdaten</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-3 p-4 rounded-lg bg-slate-50/50 dark:bg-slate-800/50">
|
||||
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
<span class="text-slate-700 dark:text-slate-300">Die ordnungsgemäße Nutzung der Geräte und Ressourcen</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-3 p-4 rounded-lg bg-slate-50/50 dark:bg-slate-800/50">
|
||||
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
<span class="text-slate-700 dark:text-slate-300">Die Einhaltung der Unternehmensrichtlinien zum Umgang mit 3D-Druckern</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-3 p-4 rounded-lg bg-slate-50/50 dark:bg-slate-800/50">
|
||||
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
<span class="text-slate-700 dark:text-slate-300">Die Beachtung von Urheberrechten und Schutzrechten Dritter bei der Erstellung von 3D-Modellen</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section 5 -->
|
||||
<section id="section-5" class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-8 scroll-mt-8">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="w-10 h-10 rounded-full bg-red-100 dark:bg-red-900/50 flex items-center justify-center">
|
||||
<span class="text-red-600 dark:text-red-400 font-bold">5</span>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Einschränkungen</h2>
|
||||
</div>
|
||||
<div class="prose dark:prose-invert max-w-none">
|
||||
<p class="text-slate-700 dark:text-slate-300 leading-relaxed mb-4">Es ist untersagt:</p>
|
||||
<div class="grid gap-3">
|
||||
<div class="flex items-start gap-3 p-4 rounded-lg bg-red-50/50 dark:bg-red-900/10 border border-red-200/50 dark:border-red-800/50">
|
||||
<svg class="w-5 h-5 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" 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>
|
||||
<span class="text-slate-700 dark:text-slate-300">Die Plattform für nicht-geschäftliche Zwecke zu nutzen</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-3 p-4 rounded-lg bg-red-50/50 dark:bg-red-900/10 border border-red-200/50 dark:border-red-800/50">
|
||||
<svg class="w-5 h-5 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" 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>
|
||||
<span class="text-slate-700 dark:text-slate-300">Unbefugten Zugang zu verschaffen</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-3 p-4 rounded-lg bg-red-50/50 dark:bg-red-900/10 border border-red-200/50 dark:border-red-800/50">
|
||||
<svg class="w-5 h-5 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" 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>
|
||||
<span class="text-slate-700 dark:text-slate-300">Die Sicherheitsmaßnahmen der Plattform zu umgehen</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-3 p-4 rounded-lg bg-red-50/50 dark:bg-red-900/10 border border-red-200/50 dark:border-red-800/50">
|
||||
<svg class="w-5 h-5 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" 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>
|
||||
<span class="text-slate-700 dark:text-slate-300">Schädlichen Code oder Malware hochzuladen</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-3 p-4 rounded-lg bg-red-50/50 dark:bg-red-900/10 border border-red-200/50 dark:border-red-800/50">
|
||||
<svg class="w-5 h-5 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" 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>
|
||||
<span class="text-slate-700 dark:text-slate-300">Die Plattform zu überlasten oder ihre normale Funktion zu stören</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Weitere Abschnitte analog... -->
|
||||
<section id="section-6" class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-8 scroll-mt-8">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="w-10 h-10 rounded-full bg-indigo-100 dark:bg-indigo-900/50 flex items-center justify-center">
|
||||
<span class="text-indigo-600 dark:text-indigo-400 font-bold">6</span>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">Verfügbarkeit und Wartung</h2>
|
||||
</div>
|
||||
<div class="prose dark:prose-invert max-w-none">
|
||||
<p class="text-slate-700 dark:text-slate-300 leading-relaxed">
|
||||
Mercedes-Benz bemüht sich um eine hohe Verfügbarkeit der MYP-Plattform, kann jedoch keine ununterbrochene Verfügbarkeit garantieren. Wartungsarbeiten werden nach Möglichkeit im Voraus angekündigt.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Kompakte weitere Abschnitte -->
|
||||
<div class="grid gap-6">
|
||||
<div class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-6">
|
||||
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-3 flex items-center gap-2">
|
||||
<span class="w-6 h-6 rounded-full bg-yellow-100 dark:bg-yellow-900/50 text-yellow-600 dark:text-yellow-400 text-sm font-bold flex items-center justify-center">7</span>
|
||||
Haftung
|
||||
</h3>
|
||||
<p class="text-slate-700 dark:text-slate-300 text-sm mb-3">Mercedes-Benz übernimmt keine Haftung für:</p>
|
||||
<ul class="text-sm text-slate-600 dark:text-slate-400 space-y-1">
|
||||
<li>• Schäden, die durch fehlerhafte Druckaufträge entstehen</li>
|
||||
<li>• Verlust von Daten oder Modellen</li>
|
||||
<li>• Ausfallzeiten der Plattform</li>
|
||||
<li>• Schäden durch unsachgemäße Verwendung der Drucker</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="glass-card bg-white/60 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/50 dark:border-slate-700/50 rounded-2xl p-6">
|
||||
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-3 flex items-center gap-2">
|
||||
<span class="w-6 h-6 rounded-full bg-cyan-100 dark:bg-cyan-900/50 text-cyan-600 dark:text-cyan-400 text-sm font-bold flex items-center justify-center">8</span>
|
||||
Datenschutz
|
||||
</h3>
|
||||
<p class="text-slate-700 dark:text-slate-300 text-sm">
|
||||
Die Erhebung und Verarbeitung personenbezogener Daten erfolgt gemäß der
|
||||
<a href="/privacy" class="text-blue-600 dark:text-blue-400 hover:underline font-medium">Datenschutzerklärung</a>.
|
||||
Die Daten werden ausschließlich zur Verwaltung und Optimierung der Druckaufträge verwendet.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kontakt Section -->
|
||||
<div class="glass-card bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 border border-blue-200/50 dark:border-blue-700/30 rounded-2xl p-8">
|
||||
<div class="text-center">
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-blue-100 dark:bg-blue-900/50 mb-6">
|
||||
<svg class="w-8 h-8 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="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-slate-900 dark:text-white mb-4">Haben Sie Fragen?</h3>
|
||||
<p class="text-slate-600 dark:text-slate-400 mb-6">Bei Fragen zu diesen Nutzungsbedingungen wenden Sie sich gerne an unser Team.</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<a href="mailto:till.tomczak@mercedes-benz.com" class="inline-flex items-center justify-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5">
|
||||
<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="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
Till Tomczak
|
||||
</a>
|
||||
<a href="mailto:torben.haack@mercedes-benz.com" class="inline-flex items-center justify-center gap-2 px-6 py-3 bg-slate-600 hover:bg-slate-700 text-white rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5">
|
||||
<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="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
Torben Haack
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<a href="javascript:history.back()" class="bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium shadow-sm hover:shadow-md transition-all duration-300">
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-end mt-12 pt-8 border-t border-slate-200 dark:border-slate-700">
|
||||
<button onclick="window.print()" class="inline-flex items-center justify-center gap-2 px-6 py-3 bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-lg transition-all duration-200">
|
||||
<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="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/>
|
||||
</svg>
|
||||
Drucken
|
||||
</button>
|
||||
<a href="javascript:history.back()" class="inline-flex items-center justify-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5">
|
||||
<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="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||
</svg>
|
||||
Zurück
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Smooth Scrolling Script -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Smooth scrolling for anchor links
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
const targetId = this.getAttribute('href');
|
||||
const targetElement = document.querySelector(targetId);
|
||||
if (targetElement) {
|
||||
targetElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
Loading…
x
Reference in New Issue
Block a user