// TeeTracker - Main Application with Nextcloud Support // ============================================ // Configuration // ============================================ const STORAGE_KEY_TEAS = 'teatracker_teas'; const STORAGE_KEY_ENTRIES = 'teatracker_entries'; const STORAGE_KEY_NC_CONFIG = 'teatracker_nextcloud_config'; // Data directory for Nextcloud storage const DATA_DIRECTORY = 'data/'; // ============================================ // Nextcloud Storage Integration // ============================================ class NextcloudStorage { constructor(baseUrl, username, password, path = '/TeeTracker/') { this.baseUrl = baseUrl.replace(/\/$/, ''); this.username = username; this.password = password; this.path = path.replace(/^\//, '/').replace(/\/$/, '/'); this.authHeader = 'Basic ' + btoa(`${username}:${password}`); this.connected = false; this.lastError = null; } getFileUrl(filename) { return `${this.baseUrl}/remote.php/dav/files/${encodeURIComponent(this.username)}${this.path}${filename}`; } getDirectoryUrl() { return `${this.baseUrl}/remote.php/dav/files/${encodeURIComponent(this.username)}${this.path}`; } async testConnection() { try { const response = await fetch(this.getDirectoryUrl(), { method: 'PROPFIND', headers: { 'Authorization': this.authHeader, 'Content-Type': 'text/xml; charset=utf-8' } }); if (response.ok) { this.connected = true; this.lastError = null; return true; } else if (response.status === 404) { await this.ensureDirectory(); this.connected = true; this.lastError = null; return true; } else { this.connected = false; this.lastError = `HTTP Error: ${response.status}`; return false; } } catch (error) { this.connected = false; this.lastError = error.message; console.error('Nextcloud connection error:', error); return false; } } // Migrate old files from root to data/ directory async migrateToDataDirectory() { try { // Check if old teas.json exists in root const oldTeasUrl = this.getFileUrl('teas.json'); const oldTeasResponse = await fetch(oldTeasUrl, { headers: { 'Authorization': this.authHeader } }); if (oldTeasResponse.ok) { const oldTeas = await oldTeasResponse.json(); await this.saveFile(DATA_DIRECTORY + 'teas.json', oldTeas); console.log('Migrated teas.json to data/ directory'); } // Check if old entries.json exists in root const oldEntriesUrl = this.getFileUrl('entries.json'); const oldEntriesResponse = await fetch(oldEntriesUrl, { headers: { 'Authorization': this.authHeader } }); if (oldEntriesResponse.ok) { const oldEntries = await oldEntriesResponse.json(); await this.saveFile(DATA_DIRECTORY + 'entries.json', oldEntries); console.log('Migrated entries.json to data/ directory'); } } catch (error) { console.log('No old files to migrate or migration failed:', error.message); } } async ensureDirectory() { try { const response = await fetch(this.getDirectoryUrl(), { method: 'MKCOL', headers: { 'Authorization': this.authHeader } }); await this.ensureSubDirectory('data'); await this.ensureSubDirectory('backup'); return response.ok || response.status === 405; } catch (error) { console.error('Error creating directory:', error); return false; } } async ensureSubDirectory(subPath) { try { const subDirUrl = `${this.getDirectoryUrl()}${subPath}/`; const response = await fetch(subDirUrl, { method: 'MKCOL', headers: { 'Authorization': this.authHeader } }); return response.ok || response.status === 405; } catch (error) { console.error('Error creating subdirectory:', error); return false; } } async loadFile(filename) { try { const url = this.getFileUrl(filename); const response = await fetch(url, { headers: { 'Authorization': this.authHeader } }); if (response.ok) { const text = await response.text(); return text ? JSON.parse(text) : null; } else if (response.status === 404) { return null; } return null; } catch (error) { console.error(`Error loading ${filename}:`, error); return null; } } async saveFile(filename, data) { try { const url = this.getFileUrl(filename); const response = await fetch(url, { method: 'PUT', headers: { 'Authorization': this.authHeader, 'Content-Type': 'application/json' }, body: JSON.stringify(data, null, 2) }); return response.ok; } catch (error) { console.error(`Error saving ${filename}:`, error); return false; } } async fileExists(filename) { try { const url = this.getFileUrl(filename); const response = await fetch(url, { method: 'HEAD', headers: { 'Authorization': this.authHeader } }); return response.ok; } catch (error) { return false; } } async createBackup(backupName, teas, entries) { try { await this.ensureSubDirectory('data'); await this.ensureSubDirectory('backup'); const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const backupData = { timestamp, teas, entries }; const filename = `backup/${backupName}_${timestamp}.json`; return await this.saveFile(filename, backupData); } catch (error) { console.error('Error creating backup:', error); return false; } } async restoreBackup(backupFilename) { try { const data = await this.loadFile(backupFilename); if (data && data.teas && data.entries) { return { teas: data.teas, entries: data.entries }; } return null; } catch (error) { console.error('Error restoring backup:', error); return null; } } } // ============================================ // Main TeeTracker Application // ============================================ class TeeTracker { constructor() { // Storage configuration this.useNextcloud = false; this.nextcloudStorage = null; this.syncInProgress = false; // Data this.teas = []; this.entries = []; this.currentTeaId = null; this.charts = {}; this.currentBgImage = null; // Initialize this.initStorage(); this.loadData().then(() => { this.init(); }); } // ============================================ // Storage Initialization // ============================================ initStorage() { // Load Nextcloud configuration from localStorage const ncConfig = localStorage.getItem(STORAGE_KEY_NC_CONFIG); const ncPassword = sessionStorage.getItem(STORAGE_KEY_NC_CONFIG + '_password'); if (ncConfig) { try { const config = JSON.parse(ncConfig); // Load password from sessionStorage (more secure than localStorage) const password = ncPassword || ''; if (password) { this.nextcloudStorage = new NextcloudStorage( config.baseUrl, config.username, password, config.path || '/TeeTracker/' ); this.useNextcloud = true; this.updateSyncStatus(); } else { // Password not available, user needs to re-enter it this.useNextcloud = false; this.showStatusMessage('Bitte gib dein Nextcloud-Passwort erneut ein.', 'info'); } } catch (error) { console.error('Invalid Nextcloud config:', error); this.useNextcloud = false; } } else { // Don't pre-fill credentials - user must enter them manually this.useNextcloud = false; } } async loadData() { if (this.useNextcloud && this.nextcloudStorage) { // Try to load from Nextcloud first await this.nextcloudStorage.ensureDirectory(); // Check if old files exist in root directory and migrate to data/ directory await this.migrateToDataDirectory(); const teas = await this.nextcloudStorage.loadFile(DATA_DIRECTORY + 'teas.json'); const entries = await this.nextcloudStorage.loadFile(DATA_DIRECTORY + 'entries.json'); if (teas) { this.teas = teas.map(tea => ({ ...tea, organic: tea.organic !== undefined ? tea.organic : false })); } if (entries) { this.entries = entries; } // If Nextcloud loading failed, fall back to localStorage if (!teas || !entries) { this.loadFromLocalStorage(); } } else { // Load from localStorage this.loadFromLocalStorage(); } } loadFromLocalStorage() { const teasData = localStorage.getItem(STORAGE_KEY_TEAS); const entriesData = localStorage.getItem(STORAGE_KEY_ENTRIES); if (teasData) { try { this.teas = JSON.parse(teasData).map(tea => ({ ...tea, organic: tea.organic !== undefined ? tea.organic : false })); } catch (error) { this.teas = []; } } if (entriesData) { try { this.entries = JSON.parse(entriesData); } catch (error) { this.entries = []; } } } async saveData() { if (this.syncInProgress) return; this.syncInProgress = true; try { if (this.useNextcloud && this.nextcloudStorage) { await this.nextcloudStorage.ensureDirectory(); // Save to Nextcloud const teasSaved = await this.nextcloudStorage.saveFile(DATA_DIRECTORY + 'teas.json', this.teas); const entriesSaved = await this.nextcloudStorage.saveFile(DATA_DIRECTORY + 'entries.json', this.entries); // Also save locally as backup this.saveToLocalStorage(); if (teasSaved && entriesSaved) { this.showToast('Daten erfolgreich mit Nextcloud synchronisiert!', 'success'); } else { this.showToast('Lokale Speicherung erfolgreich, Nextcloud-Sync fehlgeschlagen', 'warning'); } } else { // Save only locally this.saveToLocalStorage(); } } catch (error) { console.error('Error saving data:', error); this.showToast('Fehler beim Speichern der Daten', 'error'); } finally { this.syncInProgress = false; } } saveToLocalStorage() { localStorage.setItem(STORAGE_KEY_TEAS, JSON.stringify(this.teas)); localStorage.setItem(STORAGE_KEY_ENTRIES, JSON.stringify(this.entries)); } updateSyncStatus() { const statusBadge = document.getElementById('sync-status'); if (!statusBadge) return; if (this.useNextcloud) { if (this.nextcloudStorage && this.nextcloudStorage.connected) { statusBadge.textContent = 'Verbunden mit Nextcloud'; statusBadge.className = 'status-badge connected'; } else { statusBadge.textContent = 'Verbindung fehlgeschlagen'; statusBadge.className = 'status-badge disconnected'; } } else { statusBadge.textContent = 'Lokaler Modus'; statusBadge.className = 'status-badge local'; } } // ============================================ // Generate unique ID // ============================================ generateId() { return Date.now().toString(36) + Math.random().toString(36).substr(2); } // ============================================ // Get data by ID // ============================================ getTeaById(id) { return this.teas.find(tea => tea.id === id); } getTeaIndexById(id) { return this.teas.findIndex(tea => tea.id === id); } // ============================================ // Tea Management // ============================================ addTea(tea) { const newTea = { id: this.generateId(), name: tea.name, type: tea.type, brand: tea.brand || '', description: tea.description || '', color: tea.color || '#8B4513', organic: tea.organic || false, createdAt: new Date().toISOString() }; this.teas.push(newTea); this.saveData(); return newTea; } updateTea(id, updates) { const index = this.getTeaIndexById(id); if (index !== -1) { this.teas[index] = { ...this.teas[index], ...updates }; this.saveData(); return this.teas[index]; } return null; } deleteTea(id) { const index = this.getTeaIndexById(id); if (index !== -1) { this.entries = this.entries.filter(entry => entry.teaId !== id); this.teas.splice(index, 1); this.saveData(); return true; } return false; } // ============================================ // Entry Management // ============================================ addEntry(entry) { const newEntry = { id: this.generateId(), teaId: entry.teaId, date: entry.date, time: entry.time || '', amount: parseInt(entry.amount) || 1, notes: entry.notes || '', createdAt: new Date().toISOString() }; this.entries.push(newEntry); this.saveData(); return newEntry; } deleteEntry(id) { const index = this.entries.findIndex(entry => entry.id === id); if (index !== -1) { this.entries.splice(index, 1); this.saveData(); return true; } return false; } // ============================================ // Statistics // ============================================ getTotalCups() { return this.entries.reduce((sum, entry) => sum + entry.amount, 0); } getTodayCups() { const today = new Date().toISOString().split('T')[0]; return this.entries .filter(entry => entry.date === today) .reduce((sum, entry) => sum + entry.amount, 0); } getWeekCups() { const now = new Date(); const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); return this.entries .filter(entry => new Date(entry.date) >= weekAgo) .reduce((sum, entry) => sum + entry.amount, 0); } getEntriesByPeriod(period) { const now = new Date(); let startDate; switch (period) { case 'today': startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate()); break; case 'week': startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); break; case 'month': startDate = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate()); break; case 'year': startDate = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate()); break; case 'all': startDate = new Date(0); break; default: startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); } return this.entries.filter(entry => new Date(entry.date) >= startDate); } getCupsByPeriod(period) { const entries = this.getEntriesByPeriod(period); return entries.reduce((sum, entry) => sum + entry.amount, 0); } getCupsByTeaType(period = 'all') { const entries = this.getEntriesByPeriod(period); const typeCounts = {}; entries.forEach(entry => { const tea = this.getTeaById(entry.teaId); if (tea) { typeCounts[tea.type] = (typeCounts[tea.type] || 0) + entry.amount; } }); return typeCounts; } getCupsByTea(period = 'all') { const entries = this.getEntriesByPeriod(period); const teaCounts = {}; entries.forEach(entry => { const tea = this.getTeaById(entry.teaId); if (tea) { teaCounts[tea.name] = (teaCounts[tea.name] || 0) + entry.amount; } }); return teaCounts; } getDailyConsumption(period = 'week') { const entries = this.getEntriesByPeriod(period); const dailyCounts = {}; entries.forEach(entry => { const date = entry.date; dailyCounts[date] = (dailyCounts[date] || 0) + entry.amount; }); return dailyCounts; } getMostConsumedTea(period = 'all') { const teaCounts = this.getCupsByTea(period); let maxCount = 0; let mostConsumed = null; for (const [teaName, count] of Object.entries(teaCounts)) { if (count > maxCount) { maxCount = count; mostConsumed = teaName; } } return { name: mostConsumed, count: maxCount }; } countUniqueDays(entries) { const days = new Set(); entries.forEach(entry => days.add(entry.date)); return days.size; } // ============================================ // UI Initialization // ============================================ init() { this.setupEventListeners(); this.renderAll(); this.initCharts(); this.updateSettingsStats(); } setupEventListeners() { // Tab navigation document.querySelectorAll('.tab-button').forEach(button => { button.addEventListener('click', (e) => this.switchTab(e.target.dataset.tab)); }); // Add tea button if (document.getElementById('add-tea-btn')) { document.getElementById('add-tea-btn').addEventListener('click', () => this.openTeaModal()); } // Tea modal if (document.getElementById('close-modal')) { document.getElementById('close-modal').addEventListener('click', () => this.closeTeaModal()); } if (document.getElementById('tea-form')) { document.getElementById('tea-form').addEventListener('submit', (e) => this.handleTeaFormSubmit(e)); } if (document.getElementById('delete-tea-btn')) { document.getElementById('delete-tea-btn').addEventListener('click', () => this.handleDeleteTea()); } // Confirm modal if (document.getElementById('close-confirm')) { document.getElementById('close-confirm').addEventListener('click', () => this.closeConfirmModal()); } if (document.getElementById('confirm-no')) { document.getElementById('confirm-no').addEventListener('click', () => this.closeConfirmModal()); } if (document.getElementById('confirm-yes')) { document.getElementById('confirm-yes').addEventListener('click', () => this.confirmDelete()); } // Track form if (document.getElementById('track-form')) { document.getElementById('track-form').addEventListener('submit', (e) => this.handleTrackFormSubmit(e)); } if (document.getElementById('cancel-track')) { document.getElementById('cancel-track').addEventListener('click', () => this.resetTrackForm()); } // Tea search and filter if (document.getElementById('tea-search')) { document.getElementById('tea-search').addEventListener('input', (e) => this.filterTeas()); } if (document.getElementById('tea-type-filter')) { document.getElementById('tea-type-filter').addEventListener('change', (e) => this.filterTeas()); } // Stats period filter if (document.getElementById('stats-period')) { document.getElementById('stats-period').addEventListener('change', (e) => this.updateStats()); } // Close modals on outside click if (document.getElementById('tea-modal')) { document.getElementById('tea-modal').addEventListener('click', (e) => { if (e.target.id === 'tea-modal') this.closeTeaModal(); }); } if (document.getElementById('confirm-modal')) { document.getElementById('confirm-modal').addEventListener('click', (e) => { if (e.target.id === 'confirm-modal') this.closeConfirmModal(); }); } // Background image settings this.setupBackgroundListeners(); // Nextcloud settings this.setupNextcloudListeners(); } setupNextcloudListeners() { // Toggle Nextcloud config visibility const ncEnabled = document.getElementById('nc-enabled'); const ncConfig = document.getElementById('nc-config'); if (ncEnabled && ncConfig) { ncEnabled.addEventListener('change', () => { ncConfig.style.display = ncEnabled.checked ? 'block' : 'none'; }); // Load saved config const config = localStorage.getItem(STORAGE_KEY_NC_CONFIG); if (config) { try { const ncConfigData = JSON.parse(config); ncEnabled.checked = true; ncConfig.style.display = 'block'; document.getElementById('nc-url').value = ncConfigData.baseUrl || ''; document.getElementById('nc-username').value = ncConfigData.username || ''; document.getElementById('nc-password').value = ncConfigData.password || ''; document.getElementById('nc-path').value = ncConfigData.path || '/TeeTracker/'; } catch (error) { console.error('Error loading NC config:', error); } } } // Test connection button const testBtn = document.getElementById('test-nc-connection'); if (testBtn) { testBtn.addEventListener('click', async () => { await this.testNextcloudConnection(); }); } // Save config button const saveBtn = document.getElementById('save-nc-config'); if (saveBtn) { saveBtn.addEventListener('click', async () => { await this.saveNextcloudConfig(); }); } // Export data button const exportBtn = document.getElementById('export-data'); if (exportBtn) { exportBtn.addEventListener('click', () => this.exportData()); } // Import data button const importBtn = document.getElementById('import-data'); if (importBtn) { importBtn.addEventListener('click', () => this.openImportModal()); } // Close import modal button const closeImportBtn = document.getElementById('close-import-modal'); if (closeImportBtn) { closeImportBtn.addEventListener('click', () => this.closeImportModal()); } // Cancel import button const cancelImportBtn = document.getElementById('cancel-import-btn'); if (cancelImportBtn) { cancelImportBtn.addEventListener('click', () => this.closeImportModal()); } // Import data button in modal const importDataBtn = document.getElementById('import-data-btn'); if (importDataBtn) { importDataBtn.addEventListener('click', () => this.handleImportData()); } // Close import modal on outside click const importModal = document.getElementById('import-modal'); if (importModal) { importModal.addEventListener('click', (e) => { if (e.target.id === 'import-modal') this.closeImportModal(); }); } // Clear data button const clearBtn = document.getElementById('clear-data'); if (clearBtn) { clearBtn.addEventListener('click', () => this.showClearDataConfirm()); } } setupBackgroundListeners() { // Background image upload const bgUpload = document.getElementById('bg-image-upload'); if (bgUpload) { bgUpload.addEventListener('change', (e) => this.handleBackgroundUpload(e)); } // Background enable toggle const bgEnabled = document.getElementById('bg-enabled'); if (bgEnabled) { bgEnabled.addEventListener('change', () => this.toggleBackground()); } // Save background button const saveBgBtn = document.getElementById('save-bg'); if (saveBgBtn) { saveBgBtn.addEventListener('click', () => this.saveBackgroundSettings()); } // Remove background button const removeBgBtn = document.getElementById('remove-bg'); if (removeBgBtn) { removeBgBtn.addEventListener('click', () => this.removeBackground()); } // Load saved background settings this.loadBackgroundSettings(); } handleBackgroundUpload(e) { const file = e.target.files[0]; if (!file) return; const preview = document.getElementById('bg-preview-img'); const previewContainer = document.getElementById('bg-image-preview'); const removeBtn = document.getElementById('remove-bg'); if (file.type.startsWith('image/')) { const reader = new FileReader(); reader.onload = (event) => { preview.src = event.target.result; previewContainer.style.display = 'block'; removeBtn.style.display = 'inline-flex'; this.currentBgImage = event.target.result; }; reader.readAsDataURL(file); } } toggleBackground() { const enabled = document.getElementById('bg-enabled').checked; if (enabled && this.currentBgImage) { document.documentElement.style.setProperty('--bg-image', `url("${this.currentBgImage}")`); } else { document.documentElement.style.setProperty('--bg-image', 'none'); } } saveBackgroundSettings() { const enabled = document.getElementById('bg-enabled').checked; const bgImage = this.currentBgImage; if (enabled && bgImage) { localStorage.setItem('teatracker_bg_enabled', 'true'); localStorage.setItem('teatracker_bg_image', bgImage); this.showToast('Hintergrundbild gespeichert!', 'success'); } else { localStorage.removeItem('teatracker_bg_enabled'); localStorage.removeItem('teatracker_bg_image'); this.showToast('Hintergrundbild deaktiviert!', 'success'); } this.toggleBackground(); } removeBackground() { this.currentBgImage = null; document.getElementById('bg-preview-img').src = ''; document.getElementById('bg-image-preview').style.display = 'none'; document.getElementById('remove-bg').style.display = 'none'; document.getElementById('bg-enabled').checked = false; this.toggleBackground(); } loadBackgroundSettings() { this.currentBgImage = localStorage.getItem('teatracker_bg_image'); const enabled = localStorage.getItem('teatracker_bg_enabled') === 'true'; if (this.currentBgImage) { document.getElementById('bg-enabled').checked = enabled; if (enabled) { document.documentElement.style.setProperty('--bg-image', `url("${this.currentBgImage}")`); } // Show preview const preview = document.getElementById('bg-preview-img'); const previewContainer = document.getElementById('bg-image-preview'); const removeBtn = document.getElementById('remove-bg'); if (preview && previewContainer && removeBtn) { preview.src = this.currentBgImage; previewContainer.style.display = 'block'; removeBtn.style.display = 'inline-flex'; } } } // ============================================ // Nextcloud Functions // ============================================ async testNextcloudConnection() { const statusElement = document.getElementById('nc-status'); if (!statusElement) return; const url = document.getElementById('nc-url').value; const username = document.getElementById('nc-username').value; const password = document.getElementById('nc-password').value; if (!url || !username || !password) { this.showStatusMessage('Bitte fülle alle Pflichtfelder aus!', 'error'); return; } this.showStatusMessage('Verbindung wird getestet...', 'info'); try { const ncStorage = new NextcloudStorage(url, username, password); const connected = await ncStorage.testConnection(); if (connected) { this.showStatusMessage('✅ Verbindung erfolgreich! Nextcloud ist erreichbar.', 'success'); } else { this.showStatusMessage(`❌ Verbindung fehlgeschlagen: ${ncStorage.lastError}`, 'error'); } } catch (error) { this.showStatusMessage(`❌ Fehler: ${error.message}`, 'error'); } } async saveNextcloudConfig() { const statusElement = document.getElementById('nc-status'); if (!statusElement) return; const enabled = document.getElementById('nc-enabled').checked; const url = document.getElementById('nc-url').value; const username = document.getElementById('nc-username').value; const password = document.getElementById('nc-password').value; const path = document.getElementById('nc-path').value; if (enabled && (!url || !username || !password)) { this.showStatusMessage('Bitte fülle alle Pflichtfelder aus!', 'error'); return; } this.showStatusMessage('Konfiguration wird gespeichert...', 'info'); try { if (enabled) { // Test connection first const ncStorage = new NextcloudStorage(url, username, password, path); const connected = await ncStorage.testConnection(); if (!connected) { this.showStatusMessage(`❌ Verbindung fehlgeschlagen: ${ncStorage.lastError}`, 'error'); return; } // Load existing data from localStorage before switching to Nextcloud // This prevents data loss when connecting to Nextcloud for the first time const existingTeas = localStorage.getItem(STORAGE_KEY_TEAS); const existingEntries = localStorage.getItem(STORAGE_KEY_ENTRIES); if (existingTeas) { try { this.teas = JSON.parse(existingTeas).map(tea => ({ ...tea, organic: tea.organic !== undefined ? tea.organic : false })); } catch (error) { console.error('Error loading existing teas:', error); } } if (existingEntries) { try { this.entries = JSON.parse(existingEntries); } catch (error) { console.error('Error loading existing entries:', error); } } // Save config WITHOUT password for security // Password will be requested each time or stored in sessionStorage const config = { baseUrl: url, username, path }; localStorage.setItem(STORAGE_KEY_NC_CONFIG, JSON.stringify(config)); // Store password in sessionStorage (cleared when browser closes) sessionStorage.setItem(STORAGE_KEY_NC_CONFIG + '_password', password); // Update app state this.nextcloudStorage = ncStorage; this.useNextcloud = true; // Save current data to Nextcloud await this.saveData(); this.showStatusMessage('✅ Konfiguration gespeichert und Daten synchronisiert!', 'success'); } else { // Disable Nextcloud localStorage.removeItem(STORAGE_KEY_NC_CONFIG); sessionStorage.removeItem(STORAGE_KEY_NC_CONFIG + '_password'); this.useNextcloud = false; this.nextcloudStorage = null; this.showStatusMessage('✅ Nextcloud-Synchronisation deaktiviert. Daten werden lokal gespeichert.', 'success'); } this.updateSyncStatus(); this.updateSettingsStats(); } catch (error) { this.showStatusMessage(`❌ Fehler: ${error.message}`, 'error'); } } showStatusMessage(message, type = 'info') { const statusElement = document.getElementById('nc-status'); if (statusElement) { statusElement.textContent = message; statusElement.className = `status-message ${type}`; } } // ============================================ // Data Export/Import // ============================================ exportData() { const data = { teas: this.teas, entries: this.entries, exportedAt: new Date().toISOString() }; const dataStr = JSON.stringify(data, null, 2); const dataBlob = new Blob([dataStr], { type: 'application/json' }); const url = URL.createObjectURL(dataBlob); const link = document.createElement('a'); link.href = url; link.download = `teatracker_export_${new Date().toISOString().split('T')[0]}.json`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); this.showToast('Daten wurden exportiert!', 'success'); } openImportModal() { const modal = document.getElementById('import-modal'); if (modal) { document.getElementById('import-data-textarea').value = ''; modal.classList.add('active'); } } async handleImportData() { const textarea = document.getElementById('import-data-textarea'); if (!textarea) return; try { const data = JSON.parse(textarea.value); if (data.teas && data.entries) { // Merge or replace data if (confirm('Sollen die importierten Daten die bestehenden ersetzen?')) { this.teas = data.teas.map(tea => ({ ...tea, organic: tea.organic !== undefined ? tea.organic : false })); this.entries = data.entries; } else { // Merge data this.teas = [...this.teas, ...data.teas.map(tea => ({ ...tea, organic: tea.organic !== undefined ? tea.organic : false }))]; this.entries = [...this.entries, ...data.entries]; } await this.saveData(); this.closeImportModal(); this.showToast('Daten wurden erfolgreich importiert!', 'success'); this.renderAll(); this.updateSettingsStats(); } else { this.showToast('Ungültiges Datenformat!', 'error'); } } catch (error) { this.showToast('Fehler beim Importieren der Daten!', 'error'); } } closeImportModal() { const modal = document.getElementById('import-modal'); if (modal) { modal.classList.remove('active'); } } showClearDataConfirm() { const message = 'Möchtest du wirklich ALLE Daten löschen? Dieser Vorgang kann nicht rückgängig gemacht werden!'; document.getElementById('confirm-message').textContent = message; document.getElementById('confirm-modal').classList.add('active'); // Override confirm function temporarily this.tempConfirmAction = 'clearData'; } async confirmClearData() { this.teas = []; this.entries = []; // Clear both localStorage and Nextcloud localStorage.removeItem(STORAGE_KEY_TEAS); localStorage.removeItem(STORAGE_KEY_ENTRIES); if (this.useNextcloud && this.nextcloudStorage) { await this.nextcloudStorage.saveFile(DATA_DIRECTORY + 'teas.json', []); await this.nextcloudStorage.saveFile(DATA_DIRECTORY + 'entries.json', []); } this.closeConfirmModal(); this.showToast('Alle Daten wurden gelöscht!', 'success'); this.renderAll(); this.updateSettingsStats(); } // ============================================ // Tab Navigation // ============================================ switchTab(tabId) { document.querySelectorAll('.tab-button').forEach(button => { button.classList.toggle('active', button.dataset.tab === tabId); }); document.querySelectorAll('.tab-pane').forEach(pane => { pane.classList.toggle('active', pane.id === tabId); }); if (tabId === 'teas') { this.renderTeasList(); } else if (tabId === 'track') { this.renderTrackForm(); } else if (tabId === 'stats') { this.updateStats(); } else if (tabId === 'dashboard') { this.updateDashboard(); } else if (tabId === 'settings') { this.updateSettingsStats(); } } renderAll() { this.updateDashboard(); this.renderTeasList(); this.renderTrackForm(); this.updateStats(); this.updateSettingsStats(); } updateSettingsStats() { const totalCups = document.getElementById('settings-total-cups'); const totalTeas = document.getElementById('settings-total-teas'); const firstEntry = document.getElementById('settings-first-entry'); const lastEntry = document.getElementById('settings-last-entry'); if (totalCups) totalCups.textContent = this.getTotalCups(); if (totalTeas) totalTeas.textContent = this.teas.length; if (firstEntry) { if (this.entries.length > 0) { const first = this.entries.reduce((a, b) => new Date(a.createdAt) < new Date(b.createdAt) ? a : b); firstEntry.textContent = new Date(first.createdAt).toLocaleDateString('de-DE'); } else { firstEntry.textContent = '-'; } } if (lastEntry) { if (this.entries.length > 0) { const last = this.entries.reduce((a, b) => new Date(a.createdAt) > new Date(b.createdAt) ? a : b); lastEntry.textContent = new Date(last.createdAt).toLocaleDateString('de-DE'); } else { lastEntry.textContent = '-'; } } } // ============================================ // Dashboard // ============================================ updateDashboard() { const totalCups = document.getElementById('total-cups'); const todayCups = document.getElementById('today-cups'); const weekCups = document.getElementById('week-cups'); const totalTeas = document.getElementById('total-teas'); if (totalCups) totalCups.textContent = `${this.getTotalCups()} Tassen`; if (todayCups) todayCups.textContent = `${this.getTodayCups()} Tassen`; if (weekCups) weekCups.textContent = `${this.getWeekCups()} Tassen`; if (totalTeas) totalTeas.textContent = `${this.teas.length} Sorten`; this.renderRecentEntries(); } renderRecentEntries() { const container = document.getElementById('recent-entries'); if (!container) return; const recentEntries = [...this.entries] .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) .slice(0, 5); if (recentEntries.length === 0) { container.innerHTML = `