diff --git a/app.js b/app.js index 64c8575..d494f3d 100644 --- a/app.js +++ b/app.js @@ -1,60 +1,383 @@ -// TeeTracker - Main Application +// TeeTracker - Main Application with Nextcloud Support // ============================================ -// Data Storage & Management +// Configuration // ============================================ const STORAGE_KEY_TEAS = 'teatracker_teas'; const STORAGE_KEY_ENTRIES = 'teatracker_entries'; +const STORAGE_KEY_NC_CONFIG = 'teatracker_nextcloud_config'; -class TeaTracker { +// ============================================ +// 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; + } + } + + async ensureDirectory() { + try { + const response = await fetch(this.getDirectoryUrl(), { + method: 'MKCOL', + headers: { 'Authorization': this.authHeader } + }); + 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('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() { - this.teas = this.loadTeas(); - this.entries = this.loadEntries(); + // Storage configuration + this.useNextcloud = false; + this.nextcloudStorage = null; + this.syncInProgress = false; + + // Data + this.teas = []; + this.entries = []; this.currentTeaId = null; this.charts = {}; - this.init(); + // Initialize + this.initStorage(); + this.loadData().then(() => { + this.init(); + }); } - // Load data from localStorage - loadTeas() { - const data = localStorage.getItem(STORAGE_KEY_TEAS); - if (!data) return []; + // ============================================ + // Storage Initialization + // ============================================ + + initStorage() { + // Load Nextcloud configuration from localStorage + const ncConfig = localStorage.getItem(STORAGE_KEY_NC_CONFIG); + if (ncConfig) { + try { + const config = JSON.parse(ncConfig); + this.nextcloudStorage = new NextcloudStorage( + config.baseUrl, + config.username, + config.password, + config.path || '/TeeTracker/' + ); + this.useNextcloud = true; + this.updateSyncStatus(); + } catch (error) { + console.error('Invalid Nextcloud config:', error); + this.useNextcloud = false; + } + } else { + // Check if we have predefined Nextcloud credentials + const predefinedConfig = { + baseUrl: 'https://wralto.org/nextcloud3', + username: 'teetracker', + password: 'ruebennasenhausen!2026', + path: '/TeeTracker/' + }; + + // Test the predefined connection + this.nextcloudStorage = new NextcloudStorage( + predefinedConfig.baseUrl, + predefinedConfig.username, + predefinedConfig.password, + predefinedConfig.path + ); + + // Test connection asynchronously + this.nextcloudStorage.testConnection().then(connected => { + if (connected) { + this.useNextcloud = true; + localStorage.setItem(STORAGE_KEY_NC_CONFIG, JSON.stringify(predefinedConfig)); + this.updateSyncStatus(); + // Reload data from Nextcloud + this.loadData().then(() => { + this.renderAll(); + this.updateSettingsStats(); + }); + } else { + this.useNextcloud = false; + this.updateSyncStatus(); + } + }).catch(() => { + this.useNextcloud = false; + this.updateSyncStatus(); + }); + } + } + + async loadData() { + if (this.useNextcloud && this.nextcloudStorage) { + // Try to load from Nextcloud first + await this.nextcloudStorage.ensureDirectory(); + + const teas = await this.nextcloudStorage.loadFile('teas.json'); + const entries = await this.nextcloudStorage.loadFile('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); - const teas = JSON.parse(data); - // Migrate existing teas to add organic field if missing - return teas.map(tea => ({ - ...tea, - organic: tea.organic !== undefined ? tea.organic : false - })); + 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 = []; + } + } } - loadEntries() { - const data = localStorage.getItem(STORAGE_KEY_ENTRIES); - return data ? JSON.parse(data) : []; + 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('teas.json', this.teas); + const entriesSaved = await this.nextcloudStorage.saveFile('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; + } } - // Save data to localStorage - saveTeas() { + saveToLocalStorage() { localStorage.setItem(STORAGE_KEY_TEAS, JSON.stringify(this.teas)); - } - - saveEntries() { 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 tea by ID + // ============================================ + // Get data by ID + // ============================================ + getTeaById(id) { return this.teas.find(tea => tea.id === id); } - // Get tea index by ID getTeaIndexById(id) { return this.teas.findIndex(tea => tea.id === id); } @@ -75,7 +398,7 @@ class TeaTracker { createdAt: new Date().toISOString() }; this.teas.push(newTea); - this.saveTeas(); + this.saveData(); return newTea; } @@ -83,7 +406,7 @@ class TeaTracker { const index = this.getTeaIndexById(id); if (index !== -1) { this.teas[index] = { ...this.teas[index], ...updates }; - this.saveTeas(); + this.saveData(); return this.teas[index]; } return null; @@ -92,12 +415,9 @@ class TeaTracker { deleteTea(id) { const index = this.getTeaIndexById(id); if (index !== -1) { - // Remove entries that reference this tea this.entries = this.entries.filter(entry => entry.teaId !== id); - this.saveEntries(); - this.teas.splice(index, 1); - this.saveTeas(); + this.saveData(); return true; } return false; @@ -118,7 +438,7 @@ class TeaTracker { createdAt: new Date().toISOString() }; this.entries.push(newEntry); - this.saveEntries(); + this.saveData(); return newEntry; } @@ -126,7 +446,7 @@ class TeaTracker { const index = this.entries.findIndex(entry => entry.id === id); if (index !== -1) { this.entries.splice(index, 1); - this.saveEntries(); + this.saveData(); return true; } return false; @@ -147,35 +467,6 @@ class TeaTracker { .reduce((sum, entry) => sum + entry.amount, 0); } - getCupsByPeriod(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) - .reduce((sum, entry) => sum + entry.amount, 0); - } - getWeekCups() { const now = new Date(); const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); @@ -257,30 +548,6 @@ class TeaTracker { return dailyCounts; } - getEntriesByPeriod(period) { - const now = new Date(); - let startDate; - - switch (period) { - 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); - } - getMostConsumedTea(period = 'all') { const teaCounts = this.getCupsByTea(period); let maxCount = 0; @@ -296,16 +563,6 @@ class TeaTracker { return { name: mostConsumed, count: maxCount }; } - getAverageDailyConsumption(period = 'month') { - const entries = this.getEntriesByPeriod(period); - if (entries.length === 0) return 0; - - const totalDays = this.countUniqueDays(entries); - const totalCups = entries.reduce((sum, entry) => sum + entry.amount, 0); - - return Math.round(totalCups / totalDays); - } - countUniqueDays(entries) { const days = new Set(); entries.forEach(entry => days.add(entry.date)); @@ -320,6 +577,7 @@ class TeaTracker { this.setupEventListeners(); this.renderAll(); this.initCharts(); + this.updateSettingsStats(); } setupEventListeners() { @@ -329,43 +587,365 @@ class TeaTracker { }); // Add tea button - document.getElementById('add-tea-btn').addEventListener('click', () => this.openTeaModal()); + if (document.getElementById('add-tea-btn')) { + document.getElementById('add-tea-btn').addEventListener('click', () => this.openTeaModal()); + } // Tea modal - document.getElementById('close-modal').addEventListener('click', () => this.closeTeaModal()); - document.getElementById('tea-form').addEventListener('submit', (e) => this.handleTeaFormSubmit(e)); - document.getElementById('delete-tea-btn').addEventListener('click', () => this.handleDeleteTea()); + 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 - document.getElementById('close-confirm').addEventListener('click', () => this.closeConfirmModal()); - document.getElementById('confirm-no').addEventListener('click', () => this.closeConfirmModal()); - document.getElementById('confirm-yes').addEventListener('click', () => this.confirmDelete()); + 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 - document.getElementById('track-form').addEventListener('submit', (e) => this.handleTrackFormSubmit(e)); - document.getElementById('cancel-track').addEventListener('click', () => this.resetTrackForm()); + 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 - document.getElementById('tea-search').addEventListener('input', (e) => this.filterTeas()); - document.getElementById('tea-type-filter').addEventListener('change', (e) => this.filterTeas()); + 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 - document.getElementById('stats-period').addEventListener('change', (e) => this.updateStats()); + if (document.getElementById('stats-period')) { + document.getElementById('stats-period').addEventListener('change', (e) => this.updateStats()); + } // Close modals on outside click - document.getElementById('tea-modal').addEventListener('click', (e) => { - if (e.target.id === 'tea-modal') this.closeTeaModal(); - }); + if (document.getElementById('tea-modal')) { + document.getElementById('tea-modal').addEventListener('click', (e) => { + if (e.target.id === 'tea-modal') this.closeTeaModal(); + }); + } - document.getElementById('confirm-modal').addEventListener('click', (e) => { - if (e.target.id === 'confirm-modal') this.closeConfirmModal(); - }); + if (document.getElementById('confirm-modal')) { + document.getElementById('confirm-modal').addEventListener('click', (e) => { + if (e.target.id === 'confirm-modal') this.closeConfirmModal(); + }); + } + + // Nextcloud settings + this.setupNextcloudListeners(); } - switchTab(tabId) { - // Save current form data if needed + setupNextcloudListeners() { + // Toggle Nextcloud config visibility + const ncEnabled = document.getElementById('nc-enabled'); + const ncConfig = document.getElementById('nc-config'); - // Switch tabs + 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()); + } + } + + // ============================================ + // 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; + } + + // Save config + const config = { baseUrl: url, username, password, path }; + localStorage.setItem(STORAGE_KEY_NC_CONFIG, JSON.stringify(config)); + + // 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); + 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('teas.json', []); + await this.nextcloudStorage.saveFile('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); }); @@ -374,7 +954,6 @@ class TeaTracker { pane.classList.toggle('active', pane.id === tabId); }); - // Refresh content for specific tabs if (tabId === 'teas') { this.renderTeasList(); } else if (tabId === 'track') { @@ -383,6 +962,8 @@ class TeaTracker { this.updateStats(); } else if (tabId === 'dashboard') { this.updateDashboard(); + } else if (tabId === 'settings') { + this.updateSettingsStats(); } } @@ -391,6 +972,35 @@ class TeaTracker { 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 = '-'; + } + } } // ============================================ @@ -398,16 +1008,23 @@ class TeaTracker { // ============================================ updateDashboard() { - document.getElementById('total-cups').textContent = `${this.getTotalCups()} Tassen`; - document.getElementById('today-cups').textContent = `${this.getTodayCups()} Tassen`; - document.getElementById('week-cups').textContent = `${this.getWeekCups()} Tassen`; - document.getElementById('total-teas').textContent = `${this.teas.length} Sorten`; + 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); @@ -447,6 +1064,7 @@ class TeaTracker { renderTeasList() { const container = document.getElementById('teas-list'); + if (!container) return; if (this.teas.length === 0) { container.innerHTML = ` @@ -490,6 +1108,7 @@ class TeaTracker { }); const container = document.getElementById('teas-list'); + if (!container) return; if (filteredTeas.length === 0) { container.innerHTML = ` @@ -520,9 +1139,206 @@ class TeaTracker { `).join(''); } - // Render recent entries on track page + openTeaModal(teaId = null) { + const modal = document.getElementById('tea-modal'); + const form = document.getElementById('tea-form'); + const deleteBtn = document.getElementById('delete-tea-btn'); + + if (!modal || !form) return; + + if (teaId) { + const tea = this.getTeaById(teaId); + if (tea) { + document.getElementById('modal-title').textContent = 'Tee bearbeiten'; + document.getElementById('tea-id').value = tea.id; + document.getElementById('tea-name').value = tea.name; + document.getElementById('tea-type').value = tea.type; + document.getElementById('tea-brand').value = tea.brand || ''; + document.getElementById('tea-description').value = tea.description || ''; + document.getElementById('tea-color').value = tea.color || '#8B4513'; + document.getElementById('tea-organic').checked = tea.organic || false; + deleteBtn.style.display = 'inline-flex'; + this.currentTeaId = tea.id; + } + } else { + document.getElementById('modal-title').textContent = 'Tee hinzufügen'; + form.reset(); + document.getElementById('tea-id').value = ''; + deleteBtn.style.display = 'none'; + this.currentTeaId = null; + } + + modal.classList.add('active'); + } + + closeTeaModal() { + const modal = document.getElementById('tea-modal'); + if (modal) { + modal.classList.remove('active'); + const form = document.getElementById('tea-form'); + if (form) form.reset(); + this.currentTeaId = null; + } + } + + handleTeaFormSubmit(e) { + e.preventDefault(); + + const formData = { + name: document.getElementById('tea-name').value.trim(), + type: document.getElementById('tea-type').value, + brand: document.getElementById('tea-brand').value.trim(), + description: document.getElementById('tea-description').value.trim(), + color: document.getElementById('tea-color').value, + organic: document.getElementById('tea-organic').checked + }; + + const teaId = document.getElementById('tea-id').value; + + if (teaId) { + this.updateTea(teaId, formData); + this.showToast('Tee erfolgreich aktualisiert!', 'success'); + } else { + this.addTea(formData); + this.showToast('Tee erfolgreich hinzugefügt!', 'success'); + } + + this.closeTeaModal(); + this.renderTeasList(); + this.renderTrackForm(); + this.updateDashboard(); + this.updateStats(); + this.updateSettingsStats(); + } + + editTea(teaId) { + this.openTeaModal(teaId); + } + + showDeleteTeaConfirm(teaId) { + this.currentTeaId = teaId; + const tea = this.getTeaById(teaId); + if (tea) { + document.getElementById('confirm-message').textContent = + `Möchtest du den Tee "${tea.name}" wirklich löschen? Alle zugehörigen Einträge werden ebenfalls gelöscht.`; + document.getElementById('confirm-modal').classList.add('active'); + this.tempConfirmAction = 'deleteTea'; + } + } + + handleDeleteTea() { + if (this.currentTeaId) { + const tea = this.getTeaById(this.currentTeaId); + if (tea) { + document.getElementById('confirm-message').textContent = + `Möchtest du den Tee "${tea.name}" wirklich löschen? Alle zugehörigen Einträge werden ebenfalls gelöscht.`; + document.getElementById('confirm-modal').classList.add('active'); + this.tempConfirmAction = 'deleteTea'; + } + } + } + + confirmDelete() { + if (this.tempConfirmAction === 'deleteTea' && this.currentTeaId) { + const tea = this.getTeaById(this.currentTeaId); + if (tea) { + this.deleteTea(this.currentTeaId); + this.showToast(`Tee "${tea.name}" wurde gelöscht.`, 'success'); + this.closeConfirmModal(); + this.closeTeaModal(); + this.renderTeasList(); + this.renderTrackForm(); + this.updateDashboard(); + this.updateStats(); + this.updateSettingsStats(); + } + } else if (this.tempConfirmAction === 'clearData') { + this.confirmClearData(); + } + + this.tempConfirmAction = null; + } + + closeConfirmModal() { + const modal = document.getElementById('confirm-modal'); + if (modal) { + modal.classList.remove('active'); + } + this.tempConfirmAction = null; + this.currentTeaId = null; + } + + // ============================================ + // Track Form + // ============================================ + + renderTrackForm() { + const teaSelect = document.getElementById('track-tea'); + const today = new Date().toISOString().split('T')[0]; + + if (!teaSelect) return; + + document.getElementById('track-date').value = today; + + if (this.teas.length === 0) { + teaSelect.innerHTML = '-- Keine Tee-Sorten verfügbar --'; + } else { + teaSelect.innerHTML = '-- Wähle eine Tee-Sorte --' + + this.teas.map(tea => + `${tea.name} (${tea.type})${tea.organic ? ' 🌱' : ''}` + ).join(''); + } + + this.renderTrackRecentEntries(); + } + + handleTrackFormSubmit(e) { + e.preventDefault(); + + const teaId = document.getElementById('track-tea').value; + const date = document.getElementById('track-date').value; + const time = document.getElementById('track-time').value; + const amount = document.getElementById('track-amount').value; + const notes = document.getElementById('track-notes').value.trim(); + + if (!teaId || !date || !amount) { + this.showToast('Bitte fülle alle Pflichtfelder aus!', 'error'); + return; + } + + const entry = { + teaId, + date, + time, + amount, + notes + }; + + this.addEntry(entry); + this.showToast('Tee-Konsum erfolgreich getrackt!', 'success'); + + this.resetTrackForm(); + + this.updateDashboard(); + this.updateStats(); + this.renderTrackRecentEntries(); + this.updateSettingsStats(); + } + + resetTrackForm() { + const form = document.getElementById('track-form'); + if (form) { + form.reset(); + const today = new Date().toISOString().split('T')[0]; + document.getElementById('track-date').value = today; + document.getElementById('track-amount').value = 1; + } + } + renderTrackRecentEntries() { const container = document.getElementById('track-recent-entries'); + if (!container) return; + const recentEntries = [...this.entries] .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) .slice(0, 10); @@ -556,190 +1372,11 @@ class TeaTracker { }).join(''); } - openTeaModal(teaId = null) { - const modal = document.getElementById('tea-modal'); - const form = document.getElementById('tea-form'); - const deleteBtn = document.getElementById('delete-tea-btn'); - - if (teaId) { - const tea = this.getTeaById(teaId); - if (tea) { - document.getElementById('modal-title').textContent = 'Tee bearbeiten'; - document.getElementById('tea-id').value = tea.id; - document.getElementById('tea-name').value = tea.name; - document.getElementById('tea-type').value = tea.type; - document.getElementById('tea-brand').value = tea.brand || ''; - document.getElementById('tea-description').value = tea.description || ''; - document.getElementById('tea-color').value = tea.color || '#8B4513'; - document.getElementById('tea-organic').checked = tea.organic || false; - deleteBtn.style.display = 'inline-flex'; - this.currentTeaId = tea.id; - } - } else { - document.getElementById('modal-title').textContent = 'Tee hinzufügen'; - form.reset(); - document.getElementById('tea-id').value = ''; - deleteBtn.style.display = 'none'; - this.currentTeaId = null; - } - - modal.classList.add('active'); - } - - closeTeaModal() { - document.getElementById('tea-modal').classList.remove('active'); - document.getElementById('tea-form').reset(); - this.currentTeaId = null; - } - - handleTeaFormSubmit(e) { - e.preventDefault(); - - const formData = { - name: document.getElementById('tea-name').value.trim(), - type: document.getElementById('tea-type').value, - brand: document.getElementById('tea-brand').value.trim(), - description: document.getElementById('tea-description').value.trim(), - color: document.getElementById('tea-color').value, - organic: document.getElementById('tea-organic').checked - }; - - const teaId = document.getElementById('tea-id').value; - - if (teaId) { - // Update existing tea - this.updateTea(teaId, formData); - this.showToast('Tee erfolgreich aktualisiert!', 'success'); - } else { - // Add new tea - this.addTea(formData); - this.showToast('Tee erfolgreich hinzugefügt!', 'success'); - } - - this.closeTeaModal(); - this.renderTeasList(); - this.renderTrackForm(); - this.updateDashboard(); - this.updateStats(); - } - - editTea(teaId) { - this.openTeaModal(teaId); - } - - showDeleteTeaConfirm(teaId) { - this.currentTeaId = teaId; - const tea = this.getTeaById(teaId); - document.getElementById('confirm-message').textContent = - `Möchtest du den Tee "${tea.name}" wirklich löschen? Alle zugehörigen Einträge werden ebenfalls gelöscht.`; - document.getElementById('confirm-modal').classList.add('active'); - } - - handleDeleteTea() { - if (this.currentTeaId) { - const tea = this.getTeaById(this.currentTeaId); - if (tea) { - document.getElementById('confirm-message').textContent = - `Möchtest du den Tee "${tea.name}" wirklich löschen? Alle zugehörigen Einträge werden ebenfalls gelöscht.`; - document.getElementById('confirm-modal').classList.add('active'); - } - } - } - - confirmDelete() { - if (this.currentTeaId) { - const tea = this.getTeaById(this.currentTeaId); - if (tea) { - this.deleteTea(this.currentTeaId); - this.showToast(`Tee "${tea.name}" wurde gelöscht.`, 'success'); - this.closeConfirmModal(); - this.closeTeaModal(); - this.renderTeasList(); - this.renderTrackForm(); - this.updateDashboard(); - this.updateStats(); - } - } - } - - closeConfirmModal() { - document.getElementById('confirm-modal').classList.remove('active'); - this.currentTeaId = null; - } - - // ============================================ - // Track Form - // ============================================ - - renderTrackForm() { - const teaSelect = document.getElementById('track-tea'); - const today = new Date().toISOString().split('T')[0]; - - // Set default date to today - document.getElementById('track-date').value = today; - - // Populate tea dropdown - if (this.teas.length === 0) { - teaSelect.innerHTML = '-- Keine Tee-Sorten verfügbar --'; - } else { - teaSelect.innerHTML = '-- Wähle eine Tee-Sorte --' + - this.teas.map(tea => - `${tea.name} (${tea.type})${tea.organic ? ' 🌱' : ''}` - ).join(''); - } - - // Render recent entries - this.renderTrackRecentEntries(); - } - - handleTrackFormSubmit(e) { - e.preventDefault(); - - const teaId = document.getElementById('track-tea').value; - const date = document.getElementById('track-date').value; - const time = document.getElementById('track-time').value; - const amount = document.getElementById('track-amount').value; - const notes = document.getElementById('track-notes').value.trim(); - - if (!teaId || !date || !amount) { - this.showToast('Bitte fülle alle Pflichtfelder aus!', 'error'); - return; - } - - const entry = { - teaId, - date, - time, - amount, - notes - }; - - this.addEntry(entry); - this.showToast('Tee-Konsum erfolgreich getrackt!', 'success'); - - // Reset form - this.resetTrackForm(); - - // Update UI - this.updateDashboard(); - this.updateStats(); - this.renderTrackRecentEntries(); - } - - resetTrackForm() { - const form = document.getElementById('track-form'); - form.reset(); - const today = new Date().toISOString().split('T')[0]; - document.getElementById('track-date').value = today; - document.getElementById('track-amount').value = 1; - } - // ============================================ // Statistics // ============================================ initCharts() { - // Initialize Chart.js this.createTeaTypeChart(); this.createDailyChart(); } @@ -785,9 +1422,7 @@ class TeaTracker { position: 'right', labels: { padding: 15, - font: { - size: 12 - } + font: { size: 12 } } }, tooltip: { @@ -800,9 +1435,7 @@ class TeaTracker { } } }, - layout: { - padding: 20 - } + layout: { padding: 20 } } }); } @@ -814,11 +1447,12 @@ class TeaTracker { const period = document.getElementById('stats-period').value || 'week'; const dailyCounts = this.getDailyConsumption(period); - // Sort dates chronologically const sortedDates = Object.keys(dailyCounts).sort(); const labels = sortedDates.map(date => { const d = new Date(date); - return period === 'year' ? d.toLocaleDateString('de-DE', { month: 'short', day: 'numeric' }) : d.toLocaleDateString('de-DE', { weekday: 'short', day: 'numeric' }); + return period === 'year' + ? d.toLocaleDateString('de-DE', { month: 'short', day: 'numeric' }) + : d.toLocaleDateString('de-DE', { weekday: 'short', day: 'numeric' }); }); const data = sortedDates.map(date => dailyCounts[date]); @@ -842,9 +1476,7 @@ class TeaTracker { responsive: true, maintainAspectRatio: false, plugins: { - legend: { - display: false - }, + legend: { display: false }, tooltip: { callbacks: { label: function(context) { @@ -864,9 +1496,7 @@ class TeaTracker { } } }, - layout: { - padding: 20 - } + layout: { padding: 20 } } }); } @@ -874,16 +1504,15 @@ class TeaTracker { updateStats() { const period = document.getElementById('stats-period').value || 'week'; - // Update charts this.createTeaTypeChart(); this.createDailyChart(); - - // Update detailed stats this.renderDetailedStats(period); } renderDetailedStats(period) { const container = document.getElementById('stats-details'); + if (!container) return; + const entries = this.getEntriesByPeriod(period); const totalCups = entries.reduce((sum, entry) => sum + entry.amount, 0); const uniqueDays = this.countUniqueDays(entries); @@ -926,7 +1555,6 @@ class TeaTracker { // ============================================ showToast(message, type = 'info') { - // Remove existing toasts const existingToast = document.querySelector('.toast'); if (existingToast) { existingToast.remove(); @@ -947,6 +1575,6 @@ class TeaTracker { // Initialize the application let app; document.addEventListener('DOMContentLoaded', () => { - app = new TeaTracker(); + app = new TeeTracker(); window.app = app; }); diff --git a/index.html b/index.html index 94ad78d..7d0a35c 100644 --- a/index.html +++ b/index.html @@ -18,6 +18,7 @@ Tee-Sorten Tracken Statistiken + ⚙️ Einstellungen @@ -117,6 +118,90 @@ + + + ⚙️ Einstellungen + + + 🌐 Nextcloud-Synchronisation + Verbinde TeeTracker mit deiner Nextcloud-Instanz für plattformübergreifende Synchronisation. + + + + + + Nextcloud-Synchronisation aktivieren + + + + + + Nextcloud-URL * + + + + + Benutzername * + + + + + App-Passwort * + + + + + Speicherpfad + + + + + 🔍 Verbindung testen + 💾 Speichern + + + + + + + + 💾 Datenverwaltung + + + Synchronisationsstatus + Lokaler Modus + + + + 📤 Daten exportieren + 📥 Daten importieren + 🗑️ Alle Daten löschen + + + + + 📊 Statistik + + + Gesamt Tassen: + 0 + + + Tee-Sorten: + 0 + + + Erster Eintrag: + - + + + Letzter Eintrag: + - + + + + + Statistiken @@ -229,6 +314,24 @@ + + + + + + Daten importieren + × + + + Füge deine exportierten TeeTracker-Daten hier ein: + + + + + diff --git a/styles.css b/styles.css index 41de6a6..d4560e4 100644 --- a/styles.css +++ b/styles.css @@ -815,3 +815,124 @@ header p { .organic-badge:hover { transform: scale(1.1); } + +/* Settings Section */ +.settings-section { + background-color: var(--card-bg); + border-radius: var(--border-radius); + padding: 20px; + box-shadow: var(--box-shadow); + margin-bottom: 20px; +} + +.settings-section h3 { + margin-bottom: 15px; + color: var(--text-color); + border-bottom: 2px solid var(--primary-light); + padding-bottom: 10px; +} + +.settings-section p { + color: var(--text-light); + margin-bottom: 20px; + line-height: 1.6; +} + +/* Status Message */ +.status-message { + margin-top: 15px; + padding: 10px; + border-radius: var(--border-radius); + font-size: 0.9rem; +} + +.status-message.success { + background-color: rgba(40, 167, 69, 0.1); + color: var(--success-color); + border: 1px solid var(--success-color); +} + +.status-message.error { + background-color: rgba(220, 53, 69, 0.1); + color: var(--danger-color); + border: 1px solid var(--danger-color); +} + +.status-message.info { + background-color: rgba(78, 205, 196, 0.1); + color: var(--secondary-color); + border: 1px solid var(--secondary-color); +} + +/* Status Badge */ +.status-badge { + display: inline-block; + padding: 6px 12px; + border-radius: 20px; + font-size: 0.9rem; + font-weight: 600; +} + +.status-badge.local { + background-color: #6c757d; + color: white; +} + +.status-badge.connected { + background-color: var(--success-color); + color: white; +} + +.status-badge.disconnected { + background-color: var(--danger-color); + color: white; +} + +.status-badge.syncing { + background-color: var(--warning-color); + color: #333; +} + +/* Stats Overview */ +.stats-overview { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 15px; + margin-top: 15px; +} + +.stat-item { + display: flex; + justify-content: space-between; + padding: 10px; + background-color: #f9f9f9; + border-radius: var(--border-radius); +} + +.stat-label { + color: var(--text-light); + font-size: 0.9rem; +} + +.stat-value { + color: var(--text-color); + font-weight: 600; +} + +/* Modal for Import/Export */ +.modal-textarea { + width: 100%; + min-height: 200px; + font-family: monospace; + font-size: 0.85rem; + background-color: #f5f5f5; + border: 2px solid #ddd; + border-radius: var(--border-radius); + padding: 12px; + resize: vertical; +} + +.modal-textarea:focus { + outline: none; + border-color: var(--primary-color); +}
Verbinde TeeTracker mit deiner Nextcloud-Instanz für plattformübergreifende Synchronisation.
Füge deine exportierten TeeTracker-Daten hier ein: