// TeeTracker - Main Application // ============================================ // Data Storage & Management // ============================================ const STORAGE_KEY_TEAS = 'teatracker_teas'; const STORAGE_KEY_ENTRIES = 'teatracker_entries'; class TeaTracker { constructor() { this.teas = this.loadTeas(); this.entries = this.loadEntries(); this.currentTeaId = null; this.charts = {}; this.init(); } // Load data from localStorage loadTeas() { const data = localStorage.getItem(STORAGE_KEY_TEAS); return data ? JSON.parse(data) : []; } loadEntries() { const data = localStorage.getItem(STORAGE_KEY_ENTRIES); return data ? JSON.parse(data) : []; } // Save data to localStorage saveTeas() { localStorage.setItem(STORAGE_KEY_TEAS, JSON.stringify(this.teas)); } saveEntries() { localStorage.setItem(STORAGE_KEY_ENTRIES, JSON.stringify(this.entries)); } // Generate unique ID generateId() { return Date.now().toString(36) + Math.random().toString(36).substr(2); } // Get tea by ID getTeaById(id) { return this.teas.find(tea => tea.id === id); } // Get tea index by ID getTeaIndexById(id) { return this.teas.findIndex(tea => tea.id === id); } // ============================================ // 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', createdAt: new Date().toISOString() }; this.teas.push(newTea); this.saveTeas(); return newTea; } updateTea(id, updates) { const index = this.getTeaIndexById(id); if (index !== -1) { this.teas[index] = { ...this.teas[index], ...updates }; this.saveTeas(); return this.teas[index]; } return null; } deleteTea(id) { const index = this.getTeaIndexById(id); if (index !== -1) { // Remove entries that reference this tea this.entries = this.entries.filter(entry => entry.teaId !== id); this.saveEntries(); this.teas.splice(index, 1); this.saveTeas(); return true; } return false; } // ============================================ // Entry Management // ============================================ addEntry(entry) { const newEntry = { id: this.generateId(), teaId: entry.teaId, date: entry.date, time: entry.time || '', amount: parseInt(entry.amount) || 1, notes: entry.notes || '', createdAt: new Date().toISOString() }; this.entries.push(newEntry); this.saveEntries(); return newEntry; } deleteEntry(id) { const index = this.entries.findIndex(entry => entry.id === id); if (index !== -1) { this.entries.splice(index, 1); this.saveEntries(); 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); } getCupsByPeriod(period) { const now = new Date(); let startDate; switch (period) { case 'week': startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); break; case 'month': startDate = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate()); break; case 'year': startDate = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate()); break; case 'all': startDate = new Date(0); break; default: startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); } return this.entries .filter(entry => new Date(entry.date) >= startDate) .reduce((sum, entry) => sum + entry.amount, 0); } getCupsByTeaType(period = 'all') { const entries = this.getEntriesByPeriod(period); const typeCounts = {}; entries.forEach(entry => { const tea = this.getTeaById(entry.teaId); if (tea) { typeCounts[tea.type] = (typeCounts[tea.type] || 0) + entry.amount; } }); return typeCounts; } getCupsByTea(period = 'all') { const entries = this.getEntriesByPeriod(period); const teaCounts = {}; entries.forEach(entry => { const tea = this.getTeaById(entry.teaId); if (tea) { teaCounts[tea.name] = (teaCounts[tea.name] || 0) + entry.amount; } }); return teaCounts; } getDailyConsumption(period = 'week') { const entries = this.getEntriesByPeriod(period); const dailyCounts = {}; entries.forEach(entry => { const date = entry.date; dailyCounts[date] = (dailyCounts[date] || 0) + entry.amount; }); return dailyCounts; } getEntriesByPeriod(period) { const now = new Date(); let startDate; switch (period) { case 'week': startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); break; case 'month': startDate = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate()); break; case 'year': startDate = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate()); break; case 'all': startDate = new Date(0); break; default: startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); } return this.entries.filter(entry => new Date(entry.date) >= startDate); } getMostConsumedTea(period = 'all') { const teaCounts = this.getCupsByTea(period); let maxCount = 0; let mostConsumed = null; for (const [teaName, count] of Object.entries(teaCounts)) { if (count > maxCount) { maxCount = count; mostConsumed = teaName; } } return { name: mostConsumed, count: maxCount }; } getAverageDailyConsumption(period = 'month') { const entries = this.getEntriesByPeriod(period); if (entries.length === 0) return 0; const totalDays = this.countUniqueDays(entries); const totalCups = entries.reduce((sum, entry) => sum + entry.amount, 0); return Math.round(totalCups / totalDays); } countUniqueDays(entries) { const days = new Set(); entries.forEach(entry => days.add(entry.date)); return days.size; } // ============================================ // UI Initialization // ============================================ init() { this.setupEventListeners(); this.renderAll(); this.initCharts(); } setupEventListeners() { // Tab navigation document.querySelectorAll('.tab-button').forEach(button => { button.addEventListener('click', (e) => this.switchTab(e.target.dataset.tab)); }); // Add tea button document.getElementById('add-tea-btn').addEventListener('click', () => this.openTeaModal()); // Tea modal document.getElementById('close-modal').addEventListener('click', () => this.closeTeaModal()); document.getElementById('tea-form').addEventListener('submit', (e) => this.handleTeaFormSubmit(e)); document.getElementById('delete-tea-btn').addEventListener('click', () => this.handleDeleteTea()); // Confirm modal document.getElementById('close-confirm').addEventListener('click', () => this.closeConfirmModal()); document.getElementById('confirm-no').addEventListener('click', () => this.closeConfirmModal()); document.getElementById('confirm-yes').addEventListener('click', () => this.confirmDelete()); // Track form document.getElementById('track-form').addEventListener('submit', (e) => this.handleTrackFormSubmit(e)); document.getElementById('cancel-track').addEventListener('click', () => this.resetTrackForm()); // Tea search and filter document.getElementById('tea-search').addEventListener('input', (e) => this.filterTeas()); document.getElementById('tea-type-filter').addEventListener('change', (e) => this.filterTeas()); // Stats period filter document.getElementById('stats-period').addEventListener('change', (e) => this.updateStats()); // Close modals on outside click document.getElementById('tea-modal').addEventListener('click', (e) => { if (e.target.id === 'tea-modal') this.closeTeaModal(); }); document.getElementById('confirm-modal').addEventListener('click', (e) => { if (e.target.id === 'confirm-modal') this.closeConfirmModal(); }); } switchTab(tabId) { // Save current form data if needed // Switch tabs 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); }); // Refresh content for specific tabs if (tabId === 'teas') { this.renderTeasList(); } else if (tabId === 'track') { this.renderTrackForm(); } else if (tabId === 'stats') { this.updateStats(); } else if (tabId === 'dashboard') { this.updateDashboard(); } } renderAll() { this.updateDashboard(); this.renderTeasList(); this.renderTrackForm(); this.updateStats(); } // ============================================ // Dashboard // ============================================ updateDashboard() { document.getElementById('total-cups').textContent = `${this.getTotalCups()} Tassen`; document.getElementById('today-cups').textContent = `${this.getTodayCups()} Tassen`; document.getElementById('week-cups').textContent = `${this.getWeekCups()} Tassen`; document.getElementById('total-teas').textContent = `${this.teas.length} Sorten`; this.renderRecentEntries(); } renderRecentEntries() { const container = document.getElementById('recent-entries'); 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 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'}
${formattedDate} ${formattedTime ? `um ${formattedTime}` : ''}
${entry.amount} Tasse${entry.amount > 1 ? 'n' : ''}
`; }).join(''); } // ============================================ // Tea Management UI // ============================================ renderTeasList() { const container = document.getElementById('teas-list'); if (this.teas.length === 0) { container.innerHTML = `
đŸ”
Keine Tee-Sorten vorhanden
Klicke auf "Tee hinzufĂŒgen", um deine erste Sorte anzulegen
`; return; } container.innerHTML = this.teas.map(tea => `

${tea.name}

${tea.type}
${tea.brand ? `
Marke: ${tea.brand}
` : ''} ${tea.description ? `
${tea.description}
` : ''}
`).join(''); } filterTeas() { const searchTerm = document.getElementById('tea-search').value.toLowerCase(); const typeFilter = document.getElementById('tea-type-filter').value; const filteredTeas = this.teas.filter(tea => { const matchesSearch = tea.name.toLowerCase().includes(searchTerm) || tea.brand.toLowerCase().includes(searchTerm) || tea.description.toLowerCase().includes(searchTerm); const matchesType = typeFilter === 'all' || tea.type === typeFilter; return matchesSearch && matchesType; }); const container = document.getElementById('teas-list'); if (filteredTeas.length === 0) { container.innerHTML = `
🔍
Keine Tee-Sorten gefunden
Versuche andere Suchbegriffe oder Filter
`; return; } container.innerHTML = filteredTeas.map(tea => `

${tea.name}

${tea.type}
${tea.brand ? `
Marke: ${tea.brand}
` : ''} ${tea.description ? `
${tea.description}
` : ''}
`).join(''); } openTeaModal(teaId = null) { const modal = document.getElementById('tea-modal'); const form = document.getElementById('tea-form'); const deleteBtn = document.getElementById('delete-tea-btn'); if (teaId) { const tea = this.getTeaById(teaId); if (tea) { document.getElementById('modal-title').textContent = 'Tee bearbeiten'; document.getElementById('tea-id').value = tea.id; document.getElementById('tea-name').value = tea.name; document.getElementById('tea-type').value = tea.type; document.getElementById('tea-brand').value = tea.brand || ''; document.getElementById('tea-description').value = tea.description || ''; document.getElementById('tea-color').value = tea.color || '#8B4513'; deleteBtn.style.display = 'inline-flex'; this.currentTeaId = tea.id; } } else { document.getElementById('modal-title').textContent = 'Tee hinzufĂŒgen'; form.reset(); document.getElementById('tea-id').value = ''; deleteBtn.style.display = 'none'; this.currentTeaId = null; } modal.classList.add('active'); } closeTeaModal() { document.getElementById('tea-modal').classList.remove('active'); this.currentTeaId = null; } handleTeaFormSubmit(e) { e.preventDefault(); const formData = { name: document.getElementById('tea-name').value.trim(), type: document.getElementById('tea-type').value, brand: document.getElementById('tea-brand').value.trim(), description: document.getElementById('tea-description').value.trim(), color: document.getElementById('tea-color').value }; const teaId = document.getElementById('tea-id').value; if (teaId) { // Update existing tea this.updateTea(teaId, formData); this.showToast('Tee erfolgreich aktualisiert!', 'success'); } else { // Add new tea this.addTea(formData); this.showToast('Tee erfolgreich hinzugefĂŒgt!', 'success'); } this.closeTeaModal(); this.renderTeasList(); this.renderTrackForm(); this.updateDashboard(); this.updateStats(); } editTea(teaId) { this.openTeaModal(teaId); } showDeleteTeaConfirm(teaId) { this.currentTeaId = teaId; const tea = this.getTeaById(teaId); document.getElementById('confirm-message').textContent = `Möchtest du den Tee "${tea.name}" wirklich löschen? Alle zugehörigen EintrĂ€ge werden ebenfalls gelöscht.`; document.getElementById('confirm-modal').classList.add('active'); } handleDeleteTea() { if (this.currentTeaId) { const tea = this.getTeaById(this.currentTeaId); if (tea) { document.getElementById('confirm-message').textContent = `Möchtest du den Tee "${tea.name}" wirklich löschen? Alle zugehörigen EintrĂ€ge werden ebenfalls gelöscht.`; document.getElementById('confirm-modal').classList.add('active'); } } } confirmDelete() { if (this.currentTeaId) { const tea = this.getTeaById(this.currentTeaId); if (tea) { this.deleteTea(this.currentTeaId); this.showToast(`Tee "${tea.name}" wurde gelöscht.`, 'success'); this.closeConfirmModal(); this.closeTeaModal(); this.renderTeasList(); this.renderTrackForm(); this.updateDashboard(); this.updateStats(); } } } closeConfirmModal() { document.getElementById('confirm-modal').classList.remove('active'); this.currentTeaId = null; } // ============================================ // Track Form // ============================================ renderTrackForm() { const teaSelect = document.getElementById('track-tea'); const today = new Date().toISOString().split('T')[0]; // Set default date to today document.getElementById('track-date').value = today; // Populate tea dropdown if (this.teas.length === 0) { teaSelect.innerHTML = ''; } else { teaSelect.innerHTML = '' + this.teas.map(tea => `` ).join(''); } } handleTrackFormSubmit(e) { e.preventDefault(); const teaId = document.getElementById('track-tea').value; const date = document.getElementById('track-date').value; const time = document.getElementById('track-time').value; const amount = document.getElementById('track-amount').value; const notes = document.getElementById('track-notes').value.trim(); if (!teaId || !date || !amount) { this.showToast('Bitte fĂŒlle alle Pflichtfelder aus!', 'error'); return; } const entry = { teaId, date, time, amount, notes }; this.addEntry(entry); this.showToast('Tee-Konsum erfolgreich getrackt!', 'success'); // Reset form this.resetTrackForm(); // Update UI this.updateDashboard(); this.updateStats(); } resetTrackForm() { const form = document.getElementById('track-form'); form.reset(); const today = new Date().toISOString().split('T')[0]; document.getElementById('track-date').value = today; document.getElementById('track-amount').value = 1; } // ============================================ // Statistics // ============================================ initCharts() { // Initialize Chart.js this.createTeaTypeChart(); this.createDailyChart(); } createTeaTypeChart() { const ctx = document.getElementById('tea-type-chart'); if (!ctx) return; const period = document.getElementById('stats-period').value || 'week'; const typeCounts = this.getCupsByTeaType(period); const labels = Object.keys(typeCounts); const data = Object.values(typeCounts); const backgroundColors = labels.map((type, index) => { const colors = [ '#8B4513', '#228B22', '#90EE90', '#FFD700', '#FF6347', '#FF69B4', '#4169E1', '#8A2BE2' ]; return colors[index % colors.length]; }); 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: 2, borderColor: '#fff' }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', labels: { padding: 15, font: { size: 12 } } }, tooltip: { callbacks: { label: function(context) { const label = context.label || ''; const value = context.raw || 0; return `${label}: ${value} Tasse${value > 1 ? 'n' : ''}`; } } } }, layout: { padding: 20 } } }); } createDailyChart() { const ctx = document.getElementById('daily-chart'); if (!ctx) return; const period = document.getElementById('stats-period').value || 'week'; const dailyCounts = this.getDailyConsumption(period); // Sort dates chronologically const sortedDates = Object.keys(dailyCounts).sort(); const labels = sortedDates.map(date => { const d = new Date(date); return period === 'year' ? d.toLocaleDateString('de-DE', { month: 'short', day: 'numeric' }) : d.toLocaleDateString('de-DE', { weekday: 'short', day: 'numeric' }); }); const data = sortedDates.map(date => dailyCounts[date]); if (this.charts.daily) { this.charts.daily.destroy(); } this.charts.daily = new Chart(ctx, { type: 'bar', data: { labels: labels, datasets: [{ label: 'Tassen pro Tag', data: data, backgroundColor: 'rgba(139, 69, 19, 0.7)', borderColor: 'rgba(139, 69, 19, 1)', borderWidth: 2 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { callbacks: { label: function(context) { return `${context.raw} Tasse${context.raw > 1 ? 'n' : ''}`; } } } }, scales: { y: { beginAtZero: true, ticks: { stepSize: 1, callback: function(value) { return value + (value === 1 ? ' Tasse' : ' Tassen'); } } } }, layout: { padding: 20 } } }); } updateStats() { const period = document.getElementById('stats-period').value || 'week'; // Update charts this.createTeaTypeChart(); this.createDailyChart(); // Update detailed stats this.renderDetailedStats(period); } renderDetailedStats(period) { const container = document.getElementById('stats-details'); const entries = this.getEntriesByPeriod(period); const totalCups = entries.reduce((sum, entry) => sum + entry.amount, 0); const uniqueDays = this.countUniqueDays(entries); const avgDaily = uniqueDays > 0 ? Math.round(totalCups / uniqueDays) : 0; const mostConsumed = this.getMostConsumedTea(period); const teaCount = new Set(entries.map(entry => entry.teaId)).size; container.innerHTML = `
Gesamt getrunken
${totalCups} Tassen
Durchschnitt pro Tag
${avgDaily} Tassen
Aktive Tage
${uniqueDays} Tage
Verschiedene Tees
${teaCount} Sorten
${mostConsumed.name ? `
Meist getrunken
${mostConsumed.name}
Favorit (Anzahl)
${mostConsumed.count} Tassen
` : ''} `; } // ============================================ // Utility Functions // ============================================ showToast(message, type = 'info') { // Remove existing toasts const existingToast = document.querySelector('.toast'); if (existingToast) { existingToast.remove(); } const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.textContent = message; document.body.appendChild(toast); setTimeout(() => { toast.style.animation = 'toastSlideIn 0.3s ease reverse'; setTimeout(() => toast.remove(), 300); }, 3000); } } // Initialize the application let app; document.addEventListener('DOMContentLoaded', () => { app = new TeaTracker(); window.app = app; });