// 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 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) { 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) { 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 = []; } } 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)); } // 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 = `