mirror of
https://github.com/trevor1969/teatracker.git
synced 2026-08-09 10:41:59 +00:00
feat: Add TeeTracker web application
- Add index.html with responsive layout and tab navigation - Add styles.css with modern design and tea-themed colors - Add app.js with full functionality: - Tea management (add, edit, delete, search, filter) - Consumption tracking with date, time, amount, notes - Dashboard with statistics cards and recent activity - Statistics with Chart.js visualizations - Data persistence using localStorage - Toast notifications for user feedback - Confirmation modals for deletions Features: - Dashboard: Total cups, today's cups, weekly cups, tea count - Tea Management: Add, edit, delete teas with type, brand, description - Tracking: Record tea consumption with date, time, amount, notes - Statistics: Charts and detailed stats with period filtering - Responsive design for mobile and desktop - German language interface Generated by Vibe Code Co-authored-by: trevor1969 <trevor1969@users.noreply.github.com>
This commit is contained in:
866
app.js
Normal file
866
app.js
Normal file
@ -0,0 +1,866 @@
|
||||
// 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 = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">📝</div>
|
||||
<div class="empty-state-text">Keine Einträge vorhanden</div>
|
||||
<div class="empty-state-subtext">Fange an, deinen Tee-Konsum zu tracken!</div>
|
||||
</div>
|
||||
`;
|
||||
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 `
|
||||
<div class="activity-item">
|
||||
<div class="activity-info">
|
||||
<div class="activity-tea">${tea ? tea.name : 'Unbekannter Tee'}</div>
|
||||
<div class="activity-details">${formattedDate} ${formattedTime ? `um ${formattedTime}` : ''}</div>
|
||||
</div>
|
||||
<div class="activity-amount">${entry.amount} Tasse${entry.amount > 1 ? 'n' : ''}</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Tea Management UI
|
||||
// ============================================
|
||||
|
||||
renderTeasList() {
|
||||
const container = document.getElementById('teas-list');
|
||||
|
||||
if (this.teas.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">🍵</div>
|
||||
<div class="empty-state-text">Keine Tee-Sorten vorhanden</div>
|
||||
<div class="empty-state-subtext">Klicke auf "Tee hinzufügen", um deine erste Sorte anzulegen</div>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = this.teas.map(tea => `
|
||||
<div class="tea-card" data-id="${tea.id}">
|
||||
<div class="tea-card-header">
|
||||
<div>
|
||||
<h3 class="tea-name">${tea.name}</h3>
|
||||
<span class="tea-type">${tea.type}</span>
|
||||
</div>
|
||||
</div>
|
||||
${tea.brand ? `<div class="tea-brand">Marke: ${tea.brand}</div>` : ''}
|
||||
${tea.description ? `<div class="tea-description">${tea.description}</div>` : ''}
|
||||
<div class="tea-actions">
|
||||
<button class="tea-action-btn tea-edit-btn" onclick="app.editTea('${tea.id}')">Bearbeiten</button>
|
||||
<button class="tea-action-btn tea-delete-btn" onclick="app.showDeleteTeaConfirm('${tea.id}')">Löschen</button>
|
||||
</div>
|
||||
</div>
|
||||
`).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 = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">🔍</div>
|
||||
<div class="empty-state-text">Keine Tee-Sorten gefunden</div>
|
||||
<div class="empty-state-subtext">Versuche andere Suchbegriffe oder Filter</div>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = filteredTeas.map(tea => `
|
||||
<div class="tea-card" data-id="${tea.id}">
|
||||
<div class="tea-card-header">
|
||||
<div>
|
||||
<h3 class="tea-name">${tea.name}</h3>
|
||||
<span class="tea-type">${tea.type}</span>
|
||||
</div>
|
||||
</div>
|
||||
${tea.brand ? `<div class="tea-brand">Marke: ${tea.brand}</div>` : ''}
|
||||
${tea.description ? `<div class="tea-description">${tea.description}</div>` : ''}
|
||||
<div class="tea-actions">
|
||||
<button class="tea-action-btn tea-edit-btn" onclick="app.editTea('${tea.id}')">Bearbeiten</button>
|
||||
<button class="tea-action-btn tea-delete-btn" onclick="app.showDeleteTeaConfirm('${tea.id}')">Löschen</button>
|
||||
</div>
|
||||
</div>
|
||||
`).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 = '<option value="">-- Keine Tee-Sorten verfügbar --</option>';
|
||||
} else {
|
||||
teaSelect.innerHTML = '<option value="">-- Wähle eine Tee-Sorte --</option>' +
|
||||
this.teas.map(tea =>
|
||||
`<option value="${tea.id}">${tea.name} (${tea.type})</option>`
|
||||
).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 = `
|
||||
<div class="detail-item">
|
||||
<div class="detail-item-label">Gesamt getrunken</div>
|
||||
<div class="detail-item-value">${totalCups} Tassen</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="detail-item-label">Durchschnitt pro Tag</div>
|
||||
<div class="detail-item-value">${avgDaily} Tassen</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="detail-item-label">Aktive Tage</div>
|
||||
<div class="detail-item-value">${uniqueDays} Tage</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="detail-item-label">Verschiedene Tees</div>
|
||||
<div class="detail-item-value">${teaCount} Sorten</div>
|
||||
</div>
|
||||
${mostConsumed.name ? `
|
||||
<div class="detail-item">
|
||||
<div class="detail-item-label">Meist getrunken</div>
|
||||
<div class="detail-item-value">${mostConsumed.name}</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="detail-item-label">Favorit (Anzahl)</div>
|
||||
<div class="detail-item-value">${mostConsumed.count} Tassen</div>
|
||||
</div>
|
||||
` : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 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;
|
||||
});
|
||||
Reference in New Issue
Block a user