// Local File Storage // ============================================ class LocalFileStorage { constructor() { this.basePath = DATA_DIRECTORY; this.connected = false; this.lastError = null; } async testConnection() { try { // Try to create a test file const testFile = this.basePath + 'test_connection.txt'; const response = await fetch(testFile, { method: 'PUT', body: 'test' }); if (response.ok) { // Clean up test file await fetch(testFile, { method: 'DELETE' }); 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.log('Local file storage not available:', error.message); return false; } } async loadFile(filename) { try { const url = this.basePath + filename; const response = await fetch(url, { method: 'GET' }); if (response.ok) { const text = await response.text(); return text.trim() === '' ? [] : (text ? JSON.parse(text) : null); } else if (response.status === 404) { return null; } return null; } catch (error) { console.log(`Error loading ${filename}:`, error.message); return null; } } async saveFile(filename, data) { try { const url = this.basePath + filename; const response = await fetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data, null, 2) }); return response.ok; } catch (error) { console.log(`Error saving ${filename}:`, error.message); return false; } } async fileExists(filename) { try { const url = this.basePath + filename; const response = await fetch(url, { method: 'HEAD' }); return response.ok; } catch (error) { return false; } } } ======= // ============================================ // Local File Storage // ============================================ class LocalFileStorage { constructor() { this.basePath = DATA_DIRECTORY; this.connected = false; this.lastError = null; this.tested = false; } async testConnection() { if (this.tested) return this.connected; this.tested = true; try { // First, check if we can read from the data directory const testFile = this.basePath + 'teas.json'; const readResponse = await fetch(testFile, { method: 'GET' }); if (readResponse.ok) { // If we can read, try to write const testWriteFile = this.basePath + '.write_test_' + Date.now() + '.tmp'; const writeResponse = await fetch(testWriteFile, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ test: true }) }); if (writeResponse.ok) { // Clean up test file await fetch(testWriteFile, { method: 'DELETE' }).catch(() => {}); this.connected = true; this.lastError = null; return true; } } this.connected = false; this.lastError = 'Server unterstützt Dateispeicherung nicht (PUT/DELETE nicht verfügbar)'; return false; } catch (error) { this.connected = false; this.lastError = error.message; console.log('Local file storage not available:', error.message); return false; } } async loadFile(filename) { try { const url = this.basePath + filename; const response = await fetch(url, { method: 'GET', cache: 'no-store' }); if (response.ok) { const text = await response.text(); if (!text || text.trim() === '') { return []; } return JSON.parse(text); } else if (response.status === 404) { return null; } return null; } catch (error) { console.log(`Error loading ${filename}:`, error.message); return null; } } async saveFile(filename, data) { try { // First ensure the data directory exists by trying to create it // Note: We can't actually create directories via fetch, but we can try to save the file const url = this.basePath + filename; // Try with PUT first (standard for creating/updating files) let response = await fetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data, null, 2) }); if (response.ok) { return true; } // If PUT fails, try POST (some servers use POST for file creation) response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data, null, 2) }); if (response.ok) { return true; } console.log(`Failed to save ${filename}: PUT and POST both failed`); return false; } catch (error) { console.log(`Error saving ${filename}:`, error.message); return false; } } async fileExists(filename) { try { const url = this.basePath + filename; const response = await fetch(url, { method: 'HEAD' }); return response.ok; } catch (error) { return false; } } async ensureDirectory() { // Try to create a placeholder file to ensure directory exists // This works on servers that support PUT for file creation try { const placeholder = this.basePath + '.gitkeep'; const response = await fetch(placeholder, { method: 'PUT', headers: { 'Content-Type': 'text/plain' }, body: 'This file ensures the data directory exists' }); return response.ok; } catch (error) { return false; } } }TeeTracker - Main Application // Version 4.0: Local file storage in data/ directory // ============================================ // Configuration // ============================================ const STORAGE_KEY_TEAS = 'teatracker_teas'; const STORAGE_KEY_ENTRIES = 'teatracker_entries'; const DATA_DIRECTORY = 'data/'; // ============================================ // Local File Storage // ============================================ class LocalFileStorage { constructor() { this.basePath = DATA_DIRECTORY; this.connected = false; this.lastError = null; } async testConnection() { try { // Try to create a test file const testFile = this.basePath + 'test_connection.txt'; const response = await fetch(testFile, { method: 'PUT', body: 'test' }); if (response.ok) { // Clean up test file await fetch(testFile, { method: 'DELETE' }); 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.log('Local file storage not available:', error.message); return false; } } async loadFile(filename) { try { const url = this.basePath + filename; const response = await fetch(url, { method: 'GET' }); if (response.ok) { const text = await response.text(); return text.trim() === '' ? [] : (text ? JSON.parse(text) : null); } else if (response.status === 404) { return null; } return null; } catch (error) { console.log(`Error loading ${filename}:`, error.message); return null; } } async saveFile(filename, data) { try { const url = this.basePath + filename; const response = await fetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data, null, 2) }); return response.ok; } catch (error) { console.log(`Error saving ${filename}:`, error.message); return false; } } async fileExists(filename) { try { const url = this.basePath + filename; const response = await fetch(url, { method: 'HEAD' }); return response.ok; } catch (error) { return false; } } } // ============================================ // Main TeeTracker Application // ============================================ class TeeTracker { constructor() { // Storage this.fileStorage = new LocalFileStorage(); this.useFileStorage = false; this.syncInProgress = false; // Data this.teas = []; this.entries = []; this.currentTeaId = null; this.charts = {}; this.currentBgImage = null; // Tea Timer this.timer = null; this.timerSeconds = 180; this.timerRunning = false; this.timerPaused = false; this.remainingSeconds = 180; // Initialize this.initStorage(); this.setupEventListeners(); this.setupTimerListeners(); this.loadData().then(() => { this.renderAll(); this.initCharts(); this.updateSettingsStats(); }); } // ============================================ // Storage Initialization // ============================================ async initStorage() { // Test if file storage is available this.useFileStorage = await this.fileStorage.testConnection(); this.updateSyncStatus(); } async loadData() { if (this.syncInProgress) return; this.syncInProgress = true; try { if (this.useFileStorage) { // Try to ensure data directory exists await this.fileStorage.ensureDirectory(); // Try to load from file storage first const teas = await this.fileStorage.loadFile('teas.json'); const entries = await this.fileStorage.loadFile('entries.json'); // Always load from localStorage as backup const localTeas = localStorage.getItem(STORAGE_KEY_TEAS); const localEntries = localStorage.getItem(STORAGE_KEY_ENTRIES); // Load from file storage if available, otherwise use local data if (teas && Array.isArray(teas)) { this.teas = teas.map(tea => ({ ...tea, organic: tea.organic !== undefined ? tea.organic : false, rating: tea.rating !== undefined ? tea.rating : 3 })); } else if (localTeas) { try { this.teas = JSON.parse(localTeas).map(tea => ({ ...tea, organic: tea.organic !== undefined ? tea.organic : false, rating: tea.rating !== undefined ? tea.rating : 3 })); } catch (error) { console.error('Error loading local teas:', error); this.teas = []; } } else { this.teas = []; } if (entries && Array.isArray(entries)) { this.entries = entries.map(entry => ({ ...entry, teaspoons: entry.teaspoons !== undefined ? entry.teaspoons : 1 })); } else if (localEntries) { try { this.entries = JSON.parse(localEntries).map(entry => ({ ...entry, teaspoons: entry.teaspoons !== undefined ? entry.teaspoons : 1 })); } catch (error) { console.error('Error loading local entries:', error); this.entries = []; } } else { this.entries = []; } // If we loaded from file storage, save to localStorage as backup if ((teas || entries) && (localTeas === null || localEntries === null)) { this.saveToLocalStorage(); } } else { // Load from localStorage only this.loadFromLocalStorage(); } } catch (error) { console.error('Error loading data:', error); this.loadFromLocalStorage(); } finally { this.syncInProgress = false; } } 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, rating: tea.rating !== undefined ? tea.rating : 3 })); } catch (error) { this.teas = []; } } if (entriesData) { try { this.entries = JSON.parse(entriesData).map(entry => ({ ...entry, teaspoons: entry.teaspoons !== undefined ? entry.teaspoons : 1 })); } catch (error) { this.entries = []; } } } async saveData() { if (this.syncInProgress) return; this.syncInProgress = true; try { if (this.useFileStorage) { // Save to file storage const teasSaved = await this.fileStorage.saveFile('teas.json', this.teas); const entriesSaved = await this.fileStorage.saveFile('entries.json', this.entries); // Also save locally as backup this.saveToLocalStorage(); if (teasSaved && entriesSaved) { this.showToast('Daten erfolgreich in data/-Verzeichnis gespeichert!', 'success'); } else { this.showToast('Lokale Speicherung erfolgreich, Dateispeicherung 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.useFileStorage) { statusBadge.textContent = 'Dateispeicherung aktiv'; statusBadge.className = 'status-badge connected'; } else { statusBadge.textContent = 'Lokaler Modus (localStorage)'; 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, rating: tea.rating || 3, image: tea.image || '', 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, teaspoons: parseInt(entry.teaspoons) || 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': default: startDate = new Date(0); break; } 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 && tea.type) { 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.id] = { name: tea.name, type: tea.type, count: (teaCounts[tea.id] ? teaCounts[tea.id].count : 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 teaId in teaCounts) { if (teaCounts[teaId].count > maxCount) { maxCount = teaCounts[teaId].count; mostConsumed = teaCounts[teaId]; } } return mostConsumed; } countUniqueDays(entries) { const days = new Set(); entries.forEach(entry => { days.add(entry.date); }); return days.size; } // ============================================ // Initialization // ============================================ init() { // Ensure data directory exists by attempting to save this.saveData().catch(() => {}); } setupEventListeners() { // Tab navigation document.querySelectorAll('.tab-button').forEach(button => { button.addEventListener('click', () => { this.switchTab(button.dataset.tab); }); }); // Add tea button const addTeaBtn = document.getElementById('add-tea-btn'); if (addTeaBtn) { addTeaBtn.addEventListener('click', () => this.openTeaModal()); } // Tea modal const teaModal = document.getElementById('tea-modal'); if (teaModal) { teaModal.addEventListener('click', (e) => { if (e.target.id === 'tea-modal') this.closeTeaModal(); }); } // Tea form const teaForm = document.getElementById('tea-form'); if (teaForm) { teaForm.addEventListener('submit', (e) => this.handleTeaFormSubmit(e)); } // Cancel tea button const cancelTeaBtn = document.getElementById('cancel-tea-btn'); if (cancelTeaBtn) { cancelTeaBtn.addEventListener('click', () => this.closeTeaModal()); } // Track form const trackForm = document.getElementById('track-form'); if (trackForm) { trackForm.addEventListener('submit', (e) => this.handleTrackFormSubmit(e)); } // Stats period filter const statsPeriod = document.getElementById('stats-period'); if (statsPeriod) { statsPeriod.addEventListener('change', () => this.updateStats()); } // Export/Import/Clear data buttons const exportBtn = document.getElementById('export-data'); if (exportBtn) { exportBtn.addEventListener('click', () => this.exportData()); } const importBtn = document.getElementById('import-data'); if (importBtn) { importBtn.addEventListener('click', () => this.openImportModal()); } const clearBtn = document.getElementById('clear-data'); if (clearBtn) { clearBtn.addEventListener('click', () => this.showClearDataConfirm()); } // Import modal const importModal = document.getElementById('import-modal'); if (importModal) { importModal.addEventListener('click', (e) => { if (e.target.id === 'import-modal') this.closeImportModal(); }); } const closeImportBtn = document.getElementById('close-import-modal'); if (closeImportBtn) { closeImportBtn.addEventListener('click', () => this.closeImportModal()); } const cancelImportBtn = document.getElementById('cancel-import-btn'); if (cancelImportBtn) { cancelImportBtn.addEventListener('click', () => this.closeImportModal()); } const importDataBtn = document.getElementById('import-data-btn'); if (importDataBtn) { importDataBtn.addEventListener('click', () => this.handleImportData()); } // Confirm modal const confirmModal = document.getElementById('confirm-modal'); if (confirmModal) { confirmModal.addEventListener('click', (e) => { if (e.target.id === 'confirm-modal') this.closeConfirmModal(); }); } const cancelConfirmBtn = document.getElementById('confirm-no'); if (cancelConfirmBtn) { cancelConfirmBtn.addEventListener('click', () => this.closeConfirmModal()); } const confirmBtn = document.getElementById('confirm-yes'); if (confirmBtn) { confirmBtn.addEventListener('click', () => this.confirmAction()); } // Background settings this.setupBackgroundListeners(); } setupTimerListeners() { // Timer controls const startTimerBtn = document.getElementById('start-timer'); const pauseTimerBtn = document.getElementById('pause-timer'); const resetTimerBtn = document.getElementById('reset-timer'); const timerInput = document.getElementById('timer-input'); if (startTimerBtn) { startTimerBtn.addEventListener('click', () => this.startTimer()); } if (pauseTimerBtn) { pauseTimerBtn.addEventListener('click', () => this.pauseTimer()); } if (resetTimerBtn) { resetTimerBtn.addEventListener('click', () => this.resetTimer()); } if (timerInput) { timerInput.addEventListener('change', (e) => this.updateTimerFromInput(e)); timerInput.addEventListener('input', (e) => this.validateTimerInput(e)); } } 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(); } // ============================================ // Background Functions // ============================================ 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 settings = { enabled: enabled, image: this.currentBgImage || '' }; localStorage.setItem('teatracker_bg_settings', JSON.stringify(settings)); this.showToast('Hintergrundeinstellungen gespeichert!', 'success'); } removeBackground() { this.currentBgImage = null; document.getElementById('bg-image-upload').value = ''; document.getElementById('bg-preview-img').src = ''; document.getElementById('bg-image-preview').style.display = 'none'; document.getElementById('remove-bg').style.display = 'none'; document.documentElement.style.setProperty('--bg-image', 'none'); document.getElementById('bg-enabled').checked = false; } loadBackgroundSettings() { const settings = localStorage.getItem('teatracker_bg_settings'); if (settings) { try { const bgSettings = JSON.parse(settings); this.currentBgImage = bgSettings.image || null; if (bgSettings.enabled && this.currentBgImage) { document.getElementById('bg-enabled').checked = true; document.getElementById('bg-preview-img').src = this.currentBgImage; document.getElementById('bg-image-preview').style.display = 'block'; document.getElementById('remove-bg').style.display = 'inline-flex'; this.toggleBackground(); } } catch (error) { console.error('Error loading background settings:', error); } } } // ============================================ // Timer Functions // ============================================ startTimer() { if (this.timerRunning) return; if (this.timerPaused) { this.timerPaused = false; } else { const input = document.getElementById('timer-input'); if (input) { this.timerSeconds = parseInt(input.value) || 180; this.remainingSeconds = this.timerSeconds; } else { this.remainingSeconds = this.timerSeconds; } } this.timerRunning = true; this.updateTimerDisplay(); this.timer = setInterval(() => { this.remainingSeconds--; this.updateTimerDisplay(); if (this.remainingSeconds <= 0) { this.stopTimer(); this.showToast('⏰ Tee ist fertig!', 'success'); const audio = new Audio('data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2teleQAA'); audio.play().catch(() => {}); } }, 1000); } pauseTimer() { if (!this.timerRunning) return; clearInterval(this.timer); this.timerRunning = false; this.timerPaused = true; } stopTimer() { clearInterval(this.timer); this.timerRunning = false; this.timerPaused = false; } resetTimer() { this.stopTimer(); const input = document.getElementById('timer-input'); if (input) { this.timerSeconds = parseInt(input.value) || 180; } else { this.timerSeconds = 180; } this.remainingSeconds = this.timerSeconds; this.updateTimerDisplay(); } updateTimerDisplay() { const display = document.getElementById('timer-display'); if (display) { const minutes = Math.floor(this.remainingSeconds / 60); const seconds = this.remainingSeconds % 60; display.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; } } updateTimerFromInput(e) { const value = parseInt(e.target.value); if (value >= 10 && value <= 1800) { this.timerSeconds = value; this.remainingSeconds = value; this.updateTimerDisplay(); } } validateTimerInput(e) { const value = e.target.value; if (value && (parseInt(value) < 10 || parseInt(value) > 1800)) { e.target.value = this.timerSeconds; } } // ============================================ // 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'); // Set action type this.tempConfirmAction = 'clearData'; } async confirmAction() { if (this.tempConfirmAction === 'clearData') { await this.confirmClearData(); } else if (this.tempConfirmAction === 'deleteTea') { this.handleDeleteTea(); } this.tempConfirmAction = null; } async confirmClearData() { this.teas = []; this.entries = []; // Clear localStorage localStorage.removeItem(STORAGE_KEY_TEAS); localStorage.removeItem(STORAGE_KEY_ENTRIES); // Clear file storage if available if (this.useFileStorage) { await this.fileStorage.saveFile('teas.json', []); await this.fileStorage.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); }); 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'); const recentActivities = document.getElementById('recent-entries'); if (totalCups) totalCups.textContent = this.getTotalCups(); if (todayCups) todayCups.textContent = this.getTodayCups(); if (weekCups) weekCups.textContent = this.getWeekCups(); if (totalTeas) totalTeas.textContent = this.teas.length; 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 = `
📝
Keine Aktivitäten vorhanden
Fange an, deinen Tee-Konsum zu tracken!
`; return; } container.innerHTML = recentEntries.map(entry => { const tea = this.getTeaById(entry.teaId); const date = new Date(entry.date); const formattedDate = date.toLocaleDateString('de-DE'); const formattedTime = entry.time ? entry.time.substring(0, 5) : ''; return `
${tea ? tea.name : 'Unbekannter Tee'} ${tea && tea.organic ? '🌱' : ''}
${formattedDate} ${formattedTime ? `um ${formattedTime}` : ''}
${entry.amount} Tasse${entry.amount > 1 ? 'n' : ''}${entry.teaspoons ? ` (${entry.teaspoons} TL)` : ''}
`; }).join(''); } // ============================================ // Tea List // ============================================ renderTeasList() { const container = document.getElementById('teas-list'); if (!container) return; if (this.teas.length === 0) { container.innerHTML = `
🍃
Keine Tee-Sorten vorhanden
Füge deine erste Tee-Sorte hinzu!
`; // Add event listener for the add button const addBtn = container.querySelector('.add-first-tea-btn'); if (addBtn) { addBtn.addEventListener('click', () => this.openTeaModal()); } return; } this.filterTeas(); } filterTeas() { const searchInput = document.getElementById('tea-search'); const typeFilter = document.getElementById('tea-type-filter'); if (!searchInput) return; const searchTerm = searchInput.value.toLowerCase(); const selectedType = typeFilter ? typeFilter.value : 'all'; const filteredTeas = this.teas.filter(tea => { const matchesSearch = !searchTerm || tea.name.toLowerCase().includes(searchTerm) || tea.brand.toLowerCase().includes(searchTerm) || tea.description.toLowerCase().includes(searchTerm); const matchesType = selectedType === 'all' || tea.type === selectedType; return matchesSearch && matchesType; }); this.renderFilteredTeas(filteredTeas); } renderFilteredTeas(teas) { const container = document.getElementById('teas-list'); if (!container) return; if (teas.length === 0) { container.innerHTML = `
🔍
Keine Tee-Sorten gefunden
Versuche andere Suchbegriffe oder Filter
`; return; } container.innerHTML = teas.map(tea => `
${tea.name} ${tea.organic ? '🌱' : ''}
${tea.type}
${tea.brand ? `
Marke: ${tea.brand}
` : ''} ${tea.description ? `
${tea.description}
` : ''} ${tea.rating ? this.renderStarRatingStatic(tea.rating) : ''}
`).join(''); // Add event listeners to all edit and delete buttons this.setupTeaCardEventListeners(); } setupTeaCardEventListeners() { // Edit buttons document.querySelectorAll('.tea-edit-btn').forEach(btn => { btn.addEventListener('click', (e) => { const teaId = e.target.closest('.tea-edit-btn').dataset.teaId; if (teaId) this.editTea(teaId); }); }); // Delete buttons document.querySelectorAll('.tea-delete-btn').forEach(btn => { btn.addEventListener('click', (e) => { const teaId = e.target.closest('.tea-delete-btn').dataset.teaId; if (teaId) this.showDeleteTeaConfirm(teaId); }); }); } // ============================================ // Tea Modal // ============================================ openTeaModal(teaId = null) { const modal = document.getElementById('tea-modal'); const form = document.getElementById('tea-form'); if (modal) { if (teaId) { const tea = this.getTeaById(teaId); if (tea) { 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; document.getElementById('tea-image-preview-img').src = tea.image || ''; document.getElementById('tea-image-preview').style.display = tea.image ? 'block' : 'none'; this.renderStarRating(tea.rating || 3, 'tea-rating'); this.currentTeaId = tea.id; } } else { // New tea form.reset(); document.getElementById('tea-color').value = '#8B4513'; document.getElementById('tea-organic').checked = false; document.getElementById('tea-image-preview-img').src = ''; document.getElementById('tea-image-preview').style.display = 'none'; this.renderStarRating(3, 'tea-rating'); this.currentTeaId = null; } modal.classList.add('active'); } } closeTeaModal() { const modal = document.getElementById('tea-modal'); if (modal) { modal.classList.remove('active'); } this.currentTeaId = null; } handleTeaFormSubmit(e) { e.preventDefault(); const id = document.getElementById('tea-id').value; const name = document.getElementById('tea-name').value.trim(); const type = document.getElementById('tea-type').value; const brand = document.getElementById('tea-brand').value.trim(); const description = document.getElementById('tea-description').value.trim(); const color = document.getElementById('tea-color').value; const organic = document.getElementById('tea-organic').checked; const image = document.getElementById('tea-image-preview').src || ''; // Get rating from star rating const rating = this.getStarRating('tea-rating'); if (!name || !type) { this.showToast('Bitte fülle alle Pflichtfelder aus!', 'error'); return; } const teaData = { name, type, brand, description, color, organic, rating, image }; if (id) { // Update existing tea this.updateTea(id, teaData); this.showToast('Tee-Sorte erfolgreich aktualisiert!', 'success'); } else { // Add new tea this.addTea(teaData); this.showToast('Tee-Sorte erfolgreich hinzugefügt!', 'success'); } this.closeTeaModal(); this.renderTeasList(); this.renderTrackForm(); this.updateSettingsStats(); } renderStarRating(rating, containerId) { const container = document.getElementById(containerId); if (!container) return; container.innerHTML = ''; for (let i = 1; i <= 5; i++) { const star = document.createElement('span'); star.className = 'star'; star.textContent = i <= rating ? '★' : '☆'; star.dataset.value = i; star.addEventListener('click', () => this.handleStarRatingClick(star, containerId)); container.appendChild(star); } } renderStarRatingStatic(rating) { let html = '
'; for (let i = 1; i <= 5; i++) { html += `${i <= rating ? '★' : '☆'}`; } html += '
'; return html; } getStarRating(containerId) { const container = document.getElementById(containerId); if (!container) return 3; const stars = container.querySelectorAll('.star'); for (let i = 0; i < stars.length; i++) { if (stars[i].textContent === '★') { return i + 1; } } return 0; } handleStarRatingClick(star, containerId) { const value = parseInt(star.dataset.value); const container = document.getElementById(containerId); if (!container) return; const stars = container.querySelectorAll('.star'); stars.forEach((s, index) => { s.textContent = index < value ? '★' : '☆'; }); } handleImageUpload(e) { const file = e.target.files[0]; if (!file) return; const preview = document.getElementById('tea-image-preview-img'); const previewContainer = document.getElementById('tea-image-preview'); if (file.type.startsWith('image/')) { const reader = new FileReader(); reader.onload = (event) => { preview.src = event.target.result; previewContainer.style.display = 'block'; }; reader.readAsDataURL(file); } } editTea(teaId) { this.openTeaModal(teaId); } showDeleteTeaConfirm(teaId) { const tea = this.getTeaById(teaId); const message = `Möchtest du wirklich die Tee-Sorte "${tea ? tea.name : 'Unbekannt'}" löschen? Alle damit verbundenen Einträge werden ebenfalls gelöscht!`; document.getElementById('confirm-message').textContent = message; document.getElementById('confirm-modal').classList.add('active'); this.tempConfirmAction = 'deleteTea'; this.currentTeaId = teaId; } handleDeleteTea() { if (this.currentTeaId) { const tea = this.getTeaById(this.currentTeaId); if (tea) { this.deleteTea(this.currentTeaId); this.showToast(`Tee-Sorte "${tea.name}" wurde gelöscht!`, 'success'); this.renderTeasList(); this.renderTrackForm(); this.updateSettingsStats(); } } this.currentTeaId = null; } confirmDelete() { this.handleDeleteTea(); } 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 = ''; } else { teaSelect.innerHTML = '' + this.teas.map(tea => `` ).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 teaspoons = document.getElementById('track-teaspoons').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, teaspoons, 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); if (recentEntries.length === 0) { container.innerHTML = `
📝
Keine Einträge vorhanden
Fange an, deinen Tee-Konsum zu tracken!
`; return; } container.innerHTML = recentEntries.map(entry => { const tea = this.getTeaById(entry.teaId); const date = new Date(entry.date); const formattedDate = date.toLocaleDateString('de-DE'); const formattedTime = entry.time ? entry.time.substring(0, 5) : ''; return `
${tea ? tea.name : 'Unbekannter Tee'} ${tea && tea.organic ? '🌱' : ''}
${formattedDate} ${formattedTime ? `um ${formattedTime}` : ''}
${entry.amount} Tasse${entry.amount > 1 ? 'n' : ''}${entry.teaspoons ? ` (${entry.teaspoons} TL)` : ''}
`; }).join(''); } // ============================================ // Charts // ============================================ initCharts() { // Check if Chart.js is loaded if (typeof Chart === 'undefined') { console.log('Chart.js not loaded, skipping charts initialization'); return; } this.createTeaTypeChart(); this.createDailyChart(); } createTeaTypeChart() { const ctx = document.getElementById('tea-type-chart'); if (!ctx) return; const typeCounts = this.getCupsByTeaType('all'); const labels = Object.keys(typeCounts); const data = Object.values(typeCounts); // Define colors for each tea type const typeColors = { 'Schwarztee': '#8B4513', 'Grüntee': '#228B22', 'Weißtee': '#F5F5DC', 'Oolong': '#DAA520', 'Pu-Erh': '#654321', 'Früchtetee': '#FF6347', 'Kräutertee': '#9ACD32', 'Sonstiges': '#808080' }; const backgroundColors = labels.map(label => typeColors[label] || '#8B4513'); if (this.charts.teaType) { this.charts.teaType.destroy(); } this.charts.teaType = new Chart(ctx, { type: 'doughnut', data: { labels: labels, datasets: [{ data: data, backgroundColor: backgroundColors, borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: true, plugins: { legend: { position: 'bottom', labels: { font: { size: 12 }, padding: 10 } }, tooltip: { callbacks: { label: function(context) { const label = context.label || ''; const value = context.raw || 0; return `${label}: ${value} Tasse${value !== 1 ? 'n' : ''}`; } } } } } }); } createDailyChart() { const ctx = document.getElementById('daily-chart'); if (!ctx) return; const dailyCounts = this.getDailyConsumption('week'); const labels = Object.keys(dailyCounts).sort(); const data = labels.map(date => dailyCounts[date] || 0); // Format dates for display const formattedLabels = labels.map(date => { const d = new Date(date); return d.toLocaleDateString('de-DE', { weekday: 'short', day: 'numeric' }); }); if (this.charts.daily) { this.charts.daily.destroy(); } this.charts.daily = new Chart(ctx, { type: 'bar', data: { labels: formattedLabels, datasets: [{ label: 'Tassen pro Tag', data: data, backgroundColor: 'rgba(139, 69, 19, 0.7)', borderColor: 'rgba(139, 69, 19, 1)', borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: true, scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } }, plugins: { legend: { display: false }, tooltip: { callbacks: { label: function(context) { const value = context.raw || 0; return `${value} Tasse${value !== 1 ? 'n' : ''}`; } } } } } }); } // ============================================ // Statistics // ============================================ updateStats() { const period = document.getElementById('stats-period')?.value || 'all'; this.renderDetailedStats(period); // Update charts this.createTeaTypeChart(); this.createDailyChart(); } renderDetailedStats(period) { const totalCups = this.getCupsByPeriod(period); const avgPerDay = this.calculateAveragePerDay(period); const activeDays = this.calculateActiveDays(period); const uniqueTeas = this.calculateUniqueTeas(period); const mostConsumed = this.getMostConsumedTea(period); const statsContainer = document.getElementById('detailed-stats'); if (!statsContainer) return; statsContainer.innerHTML = `
${totalCups}
Gesamt getrunken
${avgPerDay.toFixed(1)}
Durchschnitt pro Tag
${activeDays}
Aktive Tage
${uniqueTeas}
Verschiedene Tees
${mostConsumed ? mostConsumed.name : '-'}
Meist getrunkener Tee
`; } calculateAveragePerDay(period) { const entries = this.getEntriesByPeriod(period); if (entries.length === 0) return 0; const totalCups = this.getCupsByPeriod(period); const days = this.countUniqueDays(entries); return days > 0 ? totalCups / days : 0; } calculateActiveDays(period) { const entries = this.getEntriesByPeriod(period); return this.countUniqueDays(entries); } calculateUniqueTeas(period) { const entries = this.getEntriesByPeriod(period); const teaIds = new Set(); entries.forEach(entry => teaIds.add(entry.teaId)); return teaIds.size; } // ============================================ // Toast Notifications // ============================================ showToast(message, type = 'info') { const toastContainer = document.getElementById('toast-container'); if (!toastContainer) return; const toast = document.createElement('div'); toast.className = `toast toast-${type}`; toast.textContent = message; toastContainer.appendChild(toast); setTimeout(() => { toast.classList.add('show'); }, 100); setTimeout(() => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 300); }, 3000); } } // Initialize application document.addEventListener('DOMContentLoaded', () => { window.app = new TeeTracker(); });