diff --git a/backend/app/database/myp.db-shm b/backend/app/database/myp.db-shm index 65f48adf..0d6d9579 100644 Binary files a/backend/app/database/myp.db-shm and b/backend/app/database/myp.db-shm differ diff --git a/backend/app/database/myp.db-wal b/backend/app/database/myp.db-wal index e9cfb2dc..d6e95d8e 100644 Binary files a/backend/app/database/myp.db-wal and b/backend/app/database/myp.db-wal differ diff --git a/backend/app/static/js/advanced-components.js b/backend/app/static/js/advanced-components.js index 0519ecba..c0b75864 100644 --- a/backend/app/static/js/advanced-components.js +++ b/backend/app/static/js/advanced-components.js @@ -1 +1,752 @@ - \ No newline at end of file +/** + * MYP Platform Advanced UI Components + * Erweiterte Komponenten: Progress-Bars, File-Upload, Datepicker + * Version: 2.0.0 + */ + +(function() { + 'use strict'; + + // Namespace erweitern + window.MYP = window.MYP || {}; + window.MYP.Advanced = window.MYP.Advanced || {}; + + /** + * Progress Bar Component + */ + class ProgressBar { + constructor(container, options = {}) { + this.container = typeof container === 'string' ? document.querySelector(container) : container; + this.options = { + value: 0, + max: 100, + showLabel: true, + showPercentage: true, + animated: true, + color: 'blue', + size: 'md', + striped: false, + ...options + }; + + this.currentValue = this.options.value; + this.init(); + } + + init() { + if (!this.container) { + console.error('ProgressBar: Container nicht gefunden'); + return; + } + + this.render(); + } + + render() { + const percentage = Math.round((this.currentValue / this.options.max) * 100); + const sizeClass = this.getSizeClass(); + const colorClass = this.getColorClass(); + + this.container.innerHTML = ` +
+ ${this.options.showLabel ? ` +
+ + ${this.options.label || 'Fortschritt'} + + ${this.options.showPercentage ? ` + + ${percentage}% + + ` : ''} +
+ ` : ''} + +
+
+
+
+
+ `; + } + + getSizeClass() { + const sizes = { + 'sm': 'h-2', + 'md': 'h-3', + 'lg': 'h-4', + 'xl': 'h-6' + }; + return sizes[this.options.size] || sizes.md; + } + + getColorClass() { + const colors = { + 'blue': 'bg-blue-500', + 'green': 'bg-green-500', + 'red': 'bg-red-500', + 'yellow': 'bg-yellow-500', + 'purple': 'bg-purple-500', + 'indigo': 'bg-indigo-500' + }; + return colors[this.options.color] || colors.blue; + } + + setValue(value, animate = true) { + const oldValue = this.currentValue; + this.currentValue = Math.max(0, Math.min(this.options.max, value)); + + if (animate) { + this.animateToValue(oldValue, this.currentValue); + } else { + this.render(); + } + } + + animateToValue(from, to) { + const duration = 500; // ms + const steps = 60; + const stepValue = (to - from) / steps; + let currentStep = 0; + + const animate = () => { + if (currentStep < steps) { + this.currentValue = from + (stepValue * currentStep); + this.render(); + currentStep++; + requestAnimationFrame(animate); + } else { + this.currentValue = to; + this.render(); + } + }; + + animate(); + } + + increment(amount = 1) { + this.setValue(this.currentValue + amount); + } + + decrement(amount = 1) { + this.setValue(this.currentValue - amount); + } + + reset() { + this.setValue(0); + } + + complete() { + this.setValue(this.options.max); + } + } + + /** + * Advanced File Upload Component + */ + class FileUpload { + constructor(container, options = {}) { + this.container = typeof container === 'string' ? document.querySelector(container) : container; + this.options = { + multiple: false, + accept: '*/*', + maxSize: 50 * 1024 * 1024, // 50MB + maxFiles: 10, + dragDrop: true, + showProgress: true, + showPreview: true, + uploadUrl: '/api/upload', + chunkSize: 1024 * 1024, // 1MB chunks + ...options + }; + + this.files = []; + this.uploads = new Map(); + this.init(); + } + + init() { + if (!this.container) { + console.error('FileUpload: Container nicht gefunden'); + return; + } + + this.render(); + this.setupEventListeners(); + } + + render() { + this.container.innerHTML = ` +
+
+
+ + + +
+ + +
+

+ ${this.getFileTypeText()} • Max. ${this.formatFileSize(this.options.maxSize)} +

+
+
+ +
+
+ `; + } + + setupEventListeners() { + const fileInput = this.container.querySelector('#fileInput'); + const dropzone = this.container.querySelector('#dropzone'); + + // File Input Change + fileInput.addEventListener('change', (e) => { + this.handleFiles(Array.from(e.target.files)); + }); + + if (this.options.dragDrop) { + // Drag and Drop Events + dropzone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropzone.classList.add('drag-over'); + }); + + dropzone.addEventListener('dragleave', (e) => { + e.preventDefault(); + dropzone.classList.remove('drag-over'); + }); + + dropzone.addEventListener('drop', (e) => { + e.preventDefault(); + dropzone.classList.remove('drag-over'); + this.handleFiles(Array.from(e.dataTransfer.files)); + }); + } + } + + handleFiles(fileList) { + for (const file of fileList) { + if (this.validateFile(file)) { + this.addFile(file); + } + } + + this.renderFileList(); + } + + validateFile(file) { + // Dateigröße prüfen + if (file.size > this.options.maxSize) { + this.showError(`Datei "${file.name}" ist zu groß. Maximum: ${this.formatFileSize(this.options.maxSize)}`); + return false; + } + + // Anzahl Dateien prüfen + if (!this.options.multiple && this.files.length > 0) { + this.files = []; // Ersetze einzelne Datei + } else if (this.files.length >= this.options.maxFiles) { + this.showError(`Maximal ${this.options.maxFiles} Dateien erlaubt`); + return false; + } + + return true; + } + + addFile(file) { + const fileData = { + id: this.generateId(), + file: file, + name: file.name, + size: file.size, + type: file.type, + status: 'pending', + progress: 0, + error: null + }; + + this.files.push(fileData); + + // Preview generieren + if (this.options.showPreview && file.type.startsWith('image/')) { + this.generatePreview(fileData); + } + } + + generatePreview(fileData) { + const reader = new FileReader(); + reader.onload = (e) => { + fileData.preview = e.target.result; + this.renderFileList(); + }; + reader.readAsDataURL(fileData.file); + } + + renderFileList() { + const fileListContainer = this.container.querySelector('#fileList'); + + if (this.files.length === 0) { + fileListContainer.innerHTML = ''; + return; + } + + fileListContainer.innerHTML = this.files.map(fileData => ` +
+
+ ${fileData.preview ? ` + Preview + ` : ` +
+ + + +
+ `} + +
+
+

+ ${fileData.name} +

+ +
+

+ ${this.formatFileSize(fileData.size)} • ${this.getStatusText(fileData.status)} +

+ + ${this.options.showProgress && fileData.status === 'uploading' ? ` +
+
+
+
+
+ ` : ''} + + ${fileData.error ? ` +

${fileData.error}

+ ` : ''} +
+
+
+ `).join(''); + + // Event Listeners für Remove-Buttons + fileListContainer.querySelectorAll('.remove-file').forEach(button => { + button.addEventListener('click', (e) => { + const fileId = e.target.closest('.remove-file').dataset.fileId; + this.removeFile(fileId); + }); + }); + } + + removeFile(fileId) { + this.files = this.files.filter(f => f.id !== fileId); + this.renderFileList(); + } + + async uploadFiles() { + const pendingFiles = this.files.filter(f => f.status === 'pending'); + + for (const fileData of pendingFiles) { + await this.uploadFile(fileData); + } + } + + async uploadFile(fileData) { + fileData.status = 'uploading'; + fileData.progress = 0; + this.renderFileList(); + + try { + const formData = new FormData(); + formData.append('file', fileData.file); + + const response = await fetch(this.options.uploadUrl, { + method: 'POST', + body: formData, + headers: { + 'X-CSRFToken': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '' + } + }); + + if (response.ok) { + fileData.status = 'completed'; + fileData.progress = 100; + const result = await response.json(); + fileData.url = result.url; + } else { + throw new Error(`Upload fehlgeschlagen: ${response.status}`); + } + } catch (error) { + fileData.status = 'error'; + fileData.error = error.message; + } + + this.renderFileList(); + } + + getFileTypeText() { + if (this.options.accept === '*/*') return 'Alle Dateitypen'; + if (this.options.accept.includes('image/')) return 'Bilder'; + if (this.options.accept.includes('.pdf')) return 'PDF-Dateien'; + return 'Spezifische Dateitypen'; + } + + getStatusText(status) { + const statusTexts = { + 'pending': 'Wartend', + 'uploading': 'Wird hochgeladen...', + 'completed': 'Abgeschlossen', + 'error': 'Fehler' + }; + return statusTexts[status] || status; + } + + formatFileSize(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + } + + generateId() { + return 'file_' + Math.random().toString(36).substr(2, 9); + } + + showError(message) { + if (window.showToast) { + window.showToast(message, 'error'); + } else { + alert(message); + } + } + + getFiles() { + return this.files; + } + + getCompletedFiles() { + return this.files.filter(f => f.status === 'completed'); + } + + clear() { + this.files = []; + this.renderFileList(); + } + } + + /** + * Simple Datepicker Component + */ + class DatePicker { + constructor(input, options = {}) { + this.input = typeof input === 'string' ? document.querySelector(input) : input; + this.options = { + format: 'dd.mm.yyyy', + minDate: null, + maxDate: null, + disabledDates: [], + language: 'de', + closeOnSelect: true, + showWeekNumbers: false, + ...options + }; + + this.isOpen = false; + this.currentDate = new Date(); + this.selectedDate = null; + this.init(); + } + + init() { + if (!this.input) { + console.error('DatePicker: Input-Element nicht gefunden'); + return; + } + + this.setupInput(); + this.createCalendar(); + this.setupEventListeners(); + } + + setupInput() { + this.input.setAttribute('readonly', 'true'); + this.input.classList.add('datepicker-input'); + + // Container für Input und Calendar + this.container = document.createElement('div'); + this.container.className = 'datepicker-container relative'; + this.input.parentNode.insertBefore(this.container, this.input); + this.container.appendChild(this.input); + } + + createCalendar() { + this.calendar = document.createElement('div'); + this.calendar.className = 'datepicker-calendar absolute top-full left-0 mt-1 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-600 rounded-lg shadow-lg z-50 hidden'; + this.calendar.innerHTML = this.renderCalendar(); + this.container.appendChild(this.calendar); + } + + renderCalendar() { + const year = this.currentDate.getFullYear(); + const month = this.currentDate.getMonth(); + const monthName = this.getMonthName(month); + + return ` +
+
+ +
+ ${monthName} ${year} +
+ +
+
+ +
+
+ ${this.getWeekdayHeaders()} +
+
+ ${this.getDaysOfMonth(year, month)} +
+
+ `; + } + + getWeekdayHeaders() { + const weekdays = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']; + return weekdays.map(day => + `
${day}
` + ).join(''); + } + + getDaysOfMonth(year, month) { + const firstDay = new Date(year, month, 1); + const lastDay = new Date(year, month + 1, 0); + const startDate = new Date(firstDay); + startDate.setDate(startDate.getDate() - ((firstDay.getDay() + 6) % 7)); + + const days = []; + const current = new Date(startDate); + + while (current <= lastDay || current.getMonth() === month) { + const isCurrentMonth = current.getMonth() === month; + const isToday = this.isToday(current); + const isSelected = this.isSelectedDate(current); + const isDisabled = this.isDisabledDate(current); + + const classes = [ + 'w-8 h-8 text-sm rounded cursor-pointer flex items-center justify-center transition-colors', + isCurrentMonth ? 'text-slate-900 dark:text-white' : 'text-slate-400 dark:text-slate-600', + isToday ? 'bg-blue-100 dark:bg-blue-900 text-blue-900 dark:text-blue-100' : '', + isSelected ? 'bg-blue-500 text-white' : '', + !isDisabled && isCurrentMonth ? 'hover:bg-slate-100 dark:hover:bg-slate-700' : '', + isDisabled ? 'cursor-not-allowed opacity-50' : '' + ].filter(Boolean); + + days.push(` +
+ ${current.getDate()} +
+ `); + + current.setDate(current.getDate() + 1); + + if (days.length >= 42) break; // Max 6 Wochen + } + + return days.join(''); + } + + setupEventListeners() { + // Input click + this.input.addEventListener('click', () => { + this.toggle(); + }); + + // Calendar clicks + this.calendar.addEventListener('click', (e) => { + if (e.target.classList.contains('prev-month')) { + this.previousMonth(); + } else if (e.target.classList.contains('next-month')) { + this.nextMonth(); + } else if (e.target.dataset.selectable) { + this.selectDate(new Date(e.target.dataset.date)); + } + }); + + // Click outside + document.addEventListener('click', (e) => { + if (!this.container.contains(e.target)) { + this.close(); + } + }); + } + + toggle() { + if (this.isOpen) { + this.close(); + } else { + this.open(); + } + } + + open() { + this.calendar.classList.remove('hidden'); + this.isOpen = true; + this.updateCalendar(); + } + + close() { + this.calendar.classList.add('hidden'); + this.isOpen = false; + } + + selectDate(date) { + this.selectedDate = new Date(date); + this.input.value = this.formatDate(date); + + // Custom Event + this.input.dispatchEvent(new CustomEvent('dateselected', { + detail: { date: new Date(date) } + })); + + if (this.options.closeOnSelect) { + this.close(); + } + } + + previousMonth() { + this.currentDate.setMonth(this.currentDate.getMonth() - 1); + this.updateCalendar(); + } + + nextMonth() { + this.currentDate.setMonth(this.currentDate.getMonth() + 1); + this.updateCalendar(); + } + + updateCalendar() { + this.calendar.innerHTML = this.renderCalendar(); + } + + isToday(date) { + const today = new Date(); + return date.toDateString() === today.toDateString(); + } + + isSelectedDate(date) { + return this.selectedDate && date.toDateString() === this.selectedDate.toDateString(); + } + + isDisabledDate(date) { + if (this.options.minDate && date < this.options.minDate) return true; + if (this.options.maxDate && date > this.options.maxDate) return true; + return this.options.disabledDates.some(disabled => + date.toDateString() === disabled.toDateString() + ); + } + + formatDate(date) { + const day = date.getDate().toString().padStart(2, '0'); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); + const year = date.getFullYear(); + + return this.options.format + .replace('dd', day) + .replace('mm', month) + .replace('yyyy', year); + } + + formatDateForData(date) { + return date.toISOString().split('T')[0]; + } + + getMonthName(monthIndex) { + const months = [ + 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', + 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember' + ]; + return months[monthIndex]; + } + + setValue(date) { + if (date) { + this.selectDate(new Date(date)); + } + } + + getValue() { + return this.selectedDate; + } + + clear() { + this.selectedDate = null; + this.input.value = ''; + } + } + + // Globale API + window.MYP.Advanced = { + ProgressBar, + FileUpload, + DatePicker, + + // Convenience Functions + createProgressBar: (container, options) => new ProgressBar(container, options), + createFileUpload: (container, options) => new FileUpload(container, options), + createDatePicker: (input, options) => new DatePicker(input, options) + }; + + // Auto-Initialize + document.addEventListener('DOMContentLoaded', function() { + // Auto-initialize datepickers + document.querySelectorAll('[data-datepicker]').forEach(input => { + const options = JSON.parse(input.dataset.datepicker || '{}'); + new DatePicker(input, options); + }); + + // Auto-initialize file uploads + document.querySelectorAll('[data-file-upload]').forEach(container => { + const options = JSON.parse(container.dataset.fileUpload || '{}'); + new FileUpload(container, options); + }); + + console.log('🚀 MYP Advanced Components geladen'); + }); + +})(); \ No newline at end of file diff --git a/backend/app/templates/base.html b/backend/app/templates/base.html index a0c045f6..f4a7ac6f 100644 --- a/backend/app/templates/base.html +++ b/backend/app/templates/base.html @@ -263,68 +263,66 @@ - {% if current_user.is_authenticated %} - +
-