Files
teatracker/app.js
2026-06-06 12:47:37 +00:00

1650 lines
58 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 = `
<div class="empty-state">
<div class="empty-state-icon">📝</div>
<div class="empty-state-text">Keine Aktivitäten 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'} ${tea && tea.organic ? '<span class="organic-badge" title="Bio-Tee">🌱</span>' : ''}</div>
<div class="activity-details">${formattedDate} ${formattedTime ? `um ${formattedTime}` : ''}</div>
</div>
<div class="activity-amount">${entry.amount} Tasse${entry.amount > 1 ? 'n' : ''}${entry.teaspoons ? ` (${entry.teaspoons} TL)` : ''}</div>
</div>
`;
}).join('');
}
renderTeasList() {
const container = document.getElementById('teas-list');
if (!container) return;
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">Füge deine erste Tee-Sorte hinzu!</div>
<button class="btn btn-primary add-first-tea-btn"> Tee hinzufügen</button>
</div>
`;
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 = `
<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 = teas.map(tea => `
<div class="tea-card" style="--tea-color: ${tea.color}" data-tea-id="${tea.id}">
<div class="tea-card-header">
<div class="tea-card-name">${tea.name} ${tea.organic ? '<span class="organic-badge" title="Bio-Tee">🌱</span>' : ''}</div>
<div class="tea-card-type">${tea.type}</div>
</div>
<div class="tea-card-body">
${tea.brand ? `<div class="tea-card-brand">Marke: ${tea.brand}</div>` : ''}
${tea.description ? `<div class="tea-card-description">${tea.description}</div>` : ''}
${tea.rating ? this.renderStarRatingStatic(tea.rating) : ''}
</div>
<div class="tea-card-footer">
<button class="btn-icon tea-edit-btn" title="Bearbeiten" data-tea-id="${tea.id}">
<span class="icon">✏️</span>
</button>
<button class="btn-icon btn-danger tea-delete-btn" title="Löschen" data-tea-id="${tea.id}">
<span class="icon">🗑️</span>
</button>
</div>
</div>
`).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 = '<div class="tea-rating">';
for (let i = 1; i <= 5; i++) {
html += `<span class="star">${i <= rating ? '★' : '☆'}</span>`;
}
html += '</div>';
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 = '<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})${tea.organic ? ' 🌱' : ''}</option>`
).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 = `
<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'} ${tea && tea.organic ? '<span class="organic-badge" title="Bio-Tee">🌱</span>' : ''}</div>
<div class="activity-details">${formattedDate} ${formattedTime ? `um ${formattedTime}` : ''}</div>
</div>
<div class="activity-amount">${entry.amount} Tasse${entry.amount > 1 ? 'n' : ''}${entry.teaspoons ? ` (${entry.teaspoons} TL)` : ''}</div>
</div>
`;
}).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 = `
<div class="stat-card">
<div class="stat-value">${totalCups}</div>
<div class="stat-label">Gesamt getrunken</div>
</div>
<div class="stat-card">
<div class="stat-value">${avgPerDay.toFixed(1)}</div>
<div class="stat-label">Durchschnitt pro Tag</div>
</div>
<div class="stat-card">
<div class="stat-value">${activeDays}</div>
<div class="stat-label">Aktive Tage</div>
</div>
<div class="stat-card">
<div class="stat-value">${uniqueTeas}</div>
<div class="stat-label">Verschiedene Tees</div>
</div>
<div class="stat-card">
<div class="stat-value">${mostConsumed ? mostConsumed.name : '-'}</div>
<div class="stat-label">Meist getrunkener Tee</div>
</div>
`;
}
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();
});