From a11eca38ab450e275775d9df5d586700dd972966 Mon Sep 17 00:00:00 2001 From: trevor1969 <51275045+trevor1969@users.noreply.github.com> Date: Sat, 6 Jun 2026 16:04:33 +0200 Subject: [PATCH] Update app.js --- app.js | 1648 -------------------------------------------------------- 1 file changed, 1648 deletions(-) diff --git a/app.js b/app.js index 9cee3ff..8b13789 100644 --- a/app.js +++ b/app.js @@ -1,1649 +1 @@ -// 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; - 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 { - 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 { - 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; - } - } -} - -// ============================================ -// 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() { - this.useFileStorage = await this.fileStorage.testConnection(); - this.updateSyncStatus(); - } - - async loadData() { - if (this.syncInProgress) return; - this.syncInProgress = true; - - try { - if (this.useFileStorage) { - await this.fileStorage.ensureDirectory(); - - const teas = await this.fileStorage.loadFile('teas.json'); - const entries = await this.fileStorage.loadFile('entries.json'); - - const localTeas = localStorage.getItem(STORAGE_KEY_TEAS); - const localEntries = localStorage.getItem(STORAGE_KEY_ENTRIES); - - 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 ((teas || entries) && (localTeas === null || localEntries === null)) { - this.saveToLocalStorage(); - } - } else { - 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) { - const teasSaved = await this.fileStorage.saveFile('teas.json', this.teas); - const entriesSaved = await this.fileStorage.saveFile('entries.json', this.entries); - - this.saveToLocalStorage(); - - if (teasSaved && entriesSaved) { - this.showToast('Daten erfolgreich in data/-Verzeichnis gespeichert!', 'success'); - } else { - this.showToast('Lokale Speicherung erfolgreich, Dateispeicherung fehlgeschlagen', 'warning'); - } - } else { - 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'; - } - } - - generateId() { - return Date.now().toString(36) + Math.random().toString(36).substr(2); - } - - getTeaById(id) { - return this.teas.find(tea => tea.id === id); - } - - getTeaIndexById(id) { - return this.teas.findIndex(tea => tea.id === id); - } - - 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; - } - - 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; - } - - 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; - } - - init() { - this.saveData().catch(() => {}); - } - - setupEventListeners() { - document.querySelectorAll('.tab-button').forEach(button => { - button.addEventListener('click', () => { - this.switchTab(button.dataset.tab); - }); - }); - - // Event delegation for tea card buttons - const teasList = document.getElementById('teas-list'); - if (teasList) { - teasList.addEventListener('click', (e) => { - const editBtn = e.target.closest('.tea-edit-btn'); - if (editBtn) { - const teaId = editBtn.dataset.teaId; - if (teaId) this.editTea(teaId); - return; - } - - const deleteBtn = e.target.closest('.tea-delete-btn'); - if (deleteBtn) { - const teaId = deleteBtn.dataset.teaId; - if (teaId) this.showDeleteTeaConfirm(teaId); - return; - } - }); - } - - const addTeaBtn = document.getElementById('add-tea-btn'); - if (addTeaBtn) { - addTeaBtn.addEventListener('click', () => this.openTeaModal()); - } - - const teaModal = document.getElementById('tea-modal'); - if (teaModal) { - teaModal.addEventListener('click', (e) => { - if (e.target.id === 'tea-modal') this.closeTeaModal(); - }); - } - - const teaForm = document.getElementById('tea-form'); - if (teaForm) { - teaForm.addEventListener('submit', (e) => this.handleTeaFormSubmit(e)); - } - - const cancelTeaBtn = document.getElementById('cancel-tea-btn'); - if (cancelTeaBtn) { - cancelTeaBtn.addEventListener('click', () => this.closeTeaModal()); - } - - const trackForm = document.getElementById('track-form'); - if (trackForm) { - trackForm.addEventListener('submit', (e) => this.handleTrackFormSubmit(e)); - } - - const statsPeriod = document.getElementById('stats-period'); - if (statsPeriod) { - statsPeriod.addEventListener('change', () => this.updateStats()); - } - - 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()); - } - - 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()); - } - - 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()); - } - - this.setupBackgroundListeners(); - } - - setupTimerListeners() { - 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() { - const bgUpload = document.getElementById('bg-image-upload'); - if (bgUpload) { - bgUpload.addEventListener('change', (e) => this.handleBackgroundUpload(e)); - } - - const bgEnabled = document.getElementById('bg-enabled'); - if (bgEnabled) { - bgEnabled.addEventListener('change', () => this.toggleBackground()); - } - - const saveBgBtn = document.getElementById('save-bg'); - if (saveBgBtn) { - saveBgBtn.addEventListener('click', () => this.saveBackgroundSettings()); - } - - const removeBgBtn = document.getElementById('remove-bg'); - if (removeBgBtn) { - removeBgBtn.addEventListener('click', () => this.removeBackground()); - } - - this.loadBackgroundSettings(); - } - - handleBackgroundUpload(e) { - const file = e.target.files[0]; - if (!file) return; - - const preview = document.getElementById('bg-preview-img'); - const previewContainer = document.getElementById('bg-image-preview'); - const removeBtn = document.getElementById('remove-bg'); - - if (file.type.startsWith('image/')) { - const reader = new FileReader(); - reader.onload = (event) => { - preview.src = event.target.result; - previewContainer.style.display = 'block'; - removeBtn.style.display = 'inline-flex'; - this.currentBgImage = event.target.result; - }; - reader.readAsDataURL(file); - } - } - - toggleBackground() { - const enabled = document.getElementById('bg-enabled').checked; - if (enabled && this.currentBgImage) { - document.documentElement.style.setProperty('--bg-image', `url("${this.currentBgImage}")`); - } else { - document.documentElement.style.setProperty('--bg-image', 'none'); - } - } - - saveBackgroundSettings() { - const enabled = document.getElementById('bg-enabled').checked; - const 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); - } - } - } - - 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; - } - } - - 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) { - 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 { - 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'); - 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 = []; - - localStorage.removeItem(STORAGE_KEY_TEAS); - localStorage.removeItem(STORAGE_KEY_ENTRIES); - - 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(); - } - - 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 = '-'; - } - } - } - - 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(''); - } - - 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!
- -
- `; - 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(''); - } - - 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 { - 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 || ''; - 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) { - this.updateTea(id, teaData); - this.showToast('Tee-Sorte erfolgreich aktualisiert!', 'success'); - } else { - 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; - } - - 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(''); - } - - initCharts() { - 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); - - 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); - - 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' : ''}`; - } - } - } - } - } - }); - } - - updateStats() { - const period = document.getElementById('stats-period')?.value || 'all'; - this.renderDetailedStats(period); - 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; - } - - 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(); -});