// 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);
if (!data) return [];
const teas = JSON.parse(data);
// Migrate existing teas to add organic field if missing
return teas.map(tea => ({
...tea,
organic: tea.organic !== undefined ? tea.organic : false
}));
}
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',
organic: tea.organic || false,
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);
}
getCupsByPeriod(period) {
const now = new Date();
let startDate;
switch (period) {
case 'today':
startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
break;
case 'week':
startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
break;
case 'month':
startDate = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate());
break;
case 'year':
startDate = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate());
break;
case 'all':
startDate = new Date(0);
break;
default:
startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
}
return this.entries
.filter(entry => new Date(entry.date) >= startDate)
.reduce((sum, entry) => sum + entry.amount, 0);
}
getWeekCups() {
const now = new Date();
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
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':
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);
}
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) {
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'} ${tea && tea.organic ? '🌱' : ''}
${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.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.brand ? `
Marke: ${tea.brand}
` : ''}
${tea.description ? `
${tea.description}
` : ''}
`).join('');
}
// Render recent entries on track page
renderTrackRecentEntries() {
const container = document.getElementById('track-recent-entries');
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' : ''}
`;
}).join('');
}
openTeaModal(teaId = null) {
const modal = document.getElementById('tea-modal');
const form = document.getElementById('tea-form');
const deleteBtn = document.getElementById('delete-tea-btn');
if (teaId) {
const tea = this.getTeaById(teaId);
if (tea) {
document.getElementById('modal-title').textContent = 'Tee bearbeiten';
document.getElementById('tea-id').value = tea.id;
document.getElementById('tea-name').value = tea.name;
document.getElementById('tea-type').value = tea.type;
document.getElementById('tea-brand').value = tea.brand || '';
document.getElementById('tea-description').value = tea.description || '';
document.getElementById('tea-color').value = tea.color || '#8B4513';
document.getElementById('tea-organic').checked = tea.organic || false;
deleteBtn.style.display = 'inline-flex';
this.currentTeaId = tea.id;
}
} else {
document.getElementById('modal-title').textContent = 'Tee hinzufügen';
form.reset();
document.getElementById('tea-id').value = '';
deleteBtn.style.display = 'none';
this.currentTeaId = null;
}
modal.classList.add('active');
}
closeTeaModal() {
document.getElementById('tea-modal').classList.remove('active');
document.getElementById('tea-form').reset();
this.currentTeaId = null;
}
handleTeaFormSubmit(e) {
e.preventDefault();
const formData = {
name: document.getElementById('tea-name').value.trim(),
type: document.getElementById('tea-type').value,
brand: document.getElementById('tea-brand').value.trim(),
description: document.getElementById('tea-description').value.trim(),
color: document.getElementById('tea-color').value,
organic: document.getElementById('tea-organic').checked
};
const teaId = document.getElementById('tea-id').value;
if (teaId) {
// Update existing tea
this.updateTea(teaId, formData);
this.showToast('Tee erfolgreich aktualisiert!', 'success');
} else {
// Add new tea
this.addTea(formData);
this.showToast('Tee erfolgreich hinzugefügt!', 'success');
}
this.closeTeaModal();
this.renderTeasList();
this.renderTrackForm();
this.updateDashboard();
this.updateStats();
}
editTea(teaId) {
this.openTeaModal(teaId);
}
showDeleteTeaConfirm(teaId) {
this.currentTeaId = teaId;
const tea = this.getTeaById(teaId);
document.getElementById('confirm-message').textContent =
`Möchtest du den Tee "${tea.name}" wirklich löschen? Alle zugehörigen Einträge werden ebenfalls gelöscht.`;
document.getElementById('confirm-modal').classList.add('active');
}
handleDeleteTea() {
if (this.currentTeaId) {
const tea = this.getTeaById(this.currentTeaId);
if (tea) {
document.getElementById('confirm-message').textContent =
`Möchtest du den Tee "${tea.name}" wirklich löschen? Alle zugehörigen Einträge werden ebenfalls gelöscht.`;
document.getElementById('confirm-modal').classList.add('active');
}
}
}
confirmDelete() {
if (this.currentTeaId) {
const tea = this.getTeaById(this.currentTeaId);
if (tea) {
this.deleteTea(this.currentTeaId);
this.showToast(`Tee "${tea.name}" wurde gelöscht.`, 'success');
this.closeConfirmModal();
this.closeTeaModal();
this.renderTeasList();
this.renderTrackForm();
this.updateDashboard();
this.updateStats();
}
}
}
closeConfirmModal() {
document.getElementById('confirm-modal').classList.remove('active');
this.currentTeaId = null;
}
// ============================================
// Track Form
// ============================================
renderTrackForm() {
const teaSelect = document.getElementById('track-tea');
const today = new Date().toISOString().split('T')[0];
// Set default date to today
document.getElementById('track-date').value = today;
// Populate tea dropdown
if (this.teas.length === 0) {
teaSelect.innerHTML = '';
} else {
teaSelect.innerHTML = '' +
this.teas.map(tea =>
``
).join('');
}
// Render recent entries
this.renderTrackRecentEntries();
}
handleTrackFormSubmit(e) {
e.preventDefault();
const teaId = document.getElementById('track-tea').value;
const date = document.getElementById('track-date').value;
const time = document.getElementById('track-time').value;
const amount = document.getElementById('track-amount').value;
const notes = document.getElementById('track-notes').value.trim();
if (!teaId || !date || !amount) {
this.showToast('Bitte fülle alle Pflichtfelder aus!', 'error');
return;
}
const entry = {
teaId,
date,
time,
amount,
notes
};
this.addEntry(entry);
this.showToast('Tee-Konsum erfolgreich getrackt!', 'success');
// Reset form
this.resetTrackForm();
// Update UI
this.updateDashboard();
this.updateStats();
this.renderTrackRecentEntries();
}
resetTrackForm() {
const form = document.getElementById('track-form');
form.reset();
const today = new Date().toISOString().split('T')[0];
document.getElementById('track-date').value = today;
document.getElementById('track-amount').value = 1;
}
// ============================================
// Statistics
// ============================================
initCharts() {
// Initialize Chart.js
this.createTeaTypeChart();
this.createDailyChart();
}
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;
});