mirror of
https://github.com/trevor1969/teatracker.git
synced 2026-08-09 10:41:59 +00:00
- Ein Event-Listener am Container statt mehrfacher Listener an jedem Button - Funktioniert für alle aktuellen und zukünftigen Buttons - Keine Probleme mit mehrfachen Event-Listenern - Einfacherer, robusterer Code Co-authored-by: trevor1969 <trevor1969@users.noreply.github.com>
1949 lines
67 KiB
JavaScript
1949 lines
67 KiB
JavaScript
// Local File Storage
|
||
// ============================================
|
||
|
||
class LocalFileStorage {
|
||
constructor() {
|
||
this.basePath = DATA_DIRECTORY;
|
||
this.connected = false;
|
||
this.lastError = null;
|
||
}
|
||
|
||
async testConnection() {
|
||
try {
|
||
// Try to create a test file
|
||
const testFile = this.basePath + 'test_connection.txt';
|
||
const response = await fetch(testFile, {
|
||
method: 'PUT',
|
||
body: 'test'
|
||
});
|
||
|
||
if (response.ok) {
|
||
// Clean up test file
|
||
await fetch(testFile, { method: 'DELETE' });
|
||
this.connected = true;
|
||
this.lastError = null;
|
||
return true;
|
||
} else {
|
||
this.connected = false;
|
||
this.lastError = `HTTP Error: ${response.status}`;
|
||
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'
|
||
});
|
||
|
||
if (response.ok) {
|
||
const text = await response.text();
|
||
return text.trim() === '' ? [] : (text ? JSON.parse(text) : null);
|
||
} 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;
|
||
const response = await fetch(url, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(data, null, 2)
|
||
});
|
||
return response.ok;
|
||
} 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;
|
||
}
|
||
}
|
||
}
|
||
=======
|
||
// ============================================
|
||
// 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 {
|
||
// First ensure the data directory exists by trying to create it
|
||
// Note: We can't actually create directories via fetch, but we can try to save the file
|
||
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 to create a placeholder file to ensure directory exists
|
||
// This works on servers that support PUT for file creation
|
||
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;
|
||
}
|
||
}
|
||
}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;
|
||
}
|
||
|
||
async testConnection() {
|
||
try {
|
||
// Try to create a test file
|
||
const testFile = this.basePath + 'test_connection.txt';
|
||
const response = await fetch(testFile, {
|
||
method: 'PUT',
|
||
body: 'test'
|
||
});
|
||
|
||
if (response.ok) {
|
||
// Clean up test file
|
||
await fetch(testFile, { method: 'DELETE' });
|
||
this.connected = true;
|
||
this.lastError = null;
|
||
return true;
|
||
} else {
|
||
this.connected = false;
|
||
this.lastError = `HTTP Error: ${response.status}`;
|
||
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'
|
||
});
|
||
|
||
if (response.ok) {
|
||
const text = await response.text();
|
||
return text.trim() === '' ? [] : (text ? JSON.parse(text) : null);
|
||
} 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;
|
||
const response = await fetch(url, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(data, null, 2)
|
||
});
|
||
return response.ok;
|
||
} 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;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// 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() {
|
||
// Test if file storage is available
|
||
this.useFileStorage = await this.fileStorage.testConnection();
|
||
this.updateSyncStatus();
|
||
}
|
||
|
||
async loadData() {
|
||
if (this.syncInProgress) return;
|
||
this.syncInProgress = true;
|
||
|
||
try {
|
||
if (this.useFileStorage) {
|
||
// Try to ensure data directory exists
|
||
await this.fileStorage.ensureDirectory();
|
||
|
||
// Try to load from file storage first
|
||
const teas = await this.fileStorage.loadFile('teas.json');
|
||
const entries = await this.fileStorage.loadFile('entries.json');
|
||
|
||
// Always load from localStorage as backup
|
||
const localTeas = localStorage.getItem(STORAGE_KEY_TEAS);
|
||
const localEntries = localStorage.getItem(STORAGE_KEY_ENTRIES);
|
||
|
||
// Load from file storage if available, otherwise use local data
|
||
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 we loaded from file storage, save to localStorage as backup
|
||
if ((teas || entries) && (localTeas === null || localEntries === null)) {
|
||
this.saveToLocalStorage();
|
||
}
|
||
} else {
|
||
// Load from localStorage only
|
||
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) {
|
||
// Save to file storage
|
||
const teasSaved = await this.fileStorage.saveFile('teas.json', this.teas);
|
||
const entriesSaved = await this.fileStorage.saveFile('entries.json', this.entries);
|
||
|
||
// Also save locally as backup
|
||
this.saveToLocalStorage();
|
||
|
||
if (teasSaved && entriesSaved) {
|
||
this.showToast('Daten erfolgreich in data/-Verzeichnis gespeichert!', 'success');
|
||
} else {
|
||
this.showToast('Lokale Speicherung erfolgreich, Dateispeicherung fehlgeschlagen', 'warning');
|
||
}
|
||
} else {
|
||
// Save only locally
|
||
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';
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// Generate unique ID
|
||
// ============================================
|
||
|
||
generateId() {
|
||
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||
}
|
||
|
||
// ============================================
|
||
// Get data by ID
|
||
// ============================================
|
||
|
||
getTeaById(id) {
|
||
return this.teas.find(tea => tea.id === 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,
|
||
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;
|
||
}
|
||
|
||
// ============================================
|
||
// Entry Management
|
||
// ============================================
|
||
|
||
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;
|
||
}
|
||
|
||
// ============================================
|
||
// 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);
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
// ============================================
|
||
// Initialization
|
||
// ============================================
|
||
|
||
init() {
|
||
// Ensure data directory exists by attempting to save
|
||
this.saveData().catch(() => {});
|
||
}
|
||
|
||
setupEventListeners() {
|
||
// Tab navigation
|
||
document.querySelectorAll('.tab-button').forEach(button => {
|
||
button.addEventListener('click', () => {
|
||
this.switchTab(button.dataset.tab);
|
||
});
|
||
});
|
||
|
||
// Event delegation for tea card buttons (edit and delete)
|
||
const teasList = document.getElementById('teas-list');
|
||
if (teasList) {
|
||
teasList.addEventListener('click', (e) => {
|
||
// Edit button
|
||
const editBtn = e.target.closest('.tea-edit-btn');
|
||
if (editBtn) {
|
||
const teaId = editBtn.dataset.teaId;
|
||
if (teaId) this.editTea(teaId);
|
||
return;
|
||
}
|
||
|
||
// Delete button
|
||
const deleteBtn = e.target.closest('.tea-delete-btn');
|
||
if (deleteBtn) {
|
||
const teaId = deleteBtn.dataset.teaId;
|
||
if (teaId) this.showDeleteTeaConfirm(teaId);
|
||
return;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Add tea button
|
||
const addTeaBtn = document.getElementById('add-tea-btn');
|
||
if (addTeaBtn) {
|
||
addTeaBtn.addEventListener('click', () => this.openTeaModal());
|
||
}
|
||
|
||
// Tea modal
|
||
const teaModal = document.getElementById('tea-modal');
|
||
if (teaModal) {
|
||
teaModal.addEventListener('click', (e) => {
|
||
if (e.target.id === 'tea-modal') this.closeTeaModal();
|
||
});
|
||
}
|
||
|
||
// Tea form
|
||
const teaForm = document.getElementById('tea-form');
|
||
if (teaForm) {
|
||
teaForm.addEventListener('submit', (e) => this.handleTeaFormSubmit(e));
|
||
}
|
||
|
||
// Cancel tea button
|
||
const cancelTeaBtn = document.getElementById('cancel-tea-btn');
|
||
if (cancelTeaBtn) {
|
||
cancelTeaBtn.addEventListener('click', () => this.closeTeaModal());
|
||
}
|
||
|
||
// Track form
|
||
const trackForm = document.getElementById('track-form');
|
||
if (trackForm) {
|
||
trackForm.addEventListener('submit', (e) => this.handleTrackFormSubmit(e));
|
||
}
|
||
|
||
// Stats period filter
|
||
const statsPeriod = document.getElementById('stats-period');
|
||
if (statsPeriod) {
|
||
statsPeriod.addEventListener('change', () => this.updateStats());
|
||
}
|
||
|
||
// Export/Import/Clear data buttons
|
||
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());
|
||
}
|
||
|
||
// Import modal
|
||
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());
|
||
}
|
||
|
||
// Confirm modal
|
||
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());
|
||
}
|
||
|
||
// Background settings
|
||
this.setupBackgroundListeners();
|
||
}
|
||
|
||
setupTimerListeners() {
|
||
// Timer controls
|
||
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() {
|
||
// Background image upload
|
||
const bgUpload = document.getElementById('bg-image-upload');
|
||
if (bgUpload) {
|
||
bgUpload.addEventListener('change', (e) => this.handleBackgroundUpload(e));
|
||
}
|
||
|
||
// Background enable toggle
|
||
const bgEnabled = document.getElementById('bg-enabled');
|
||
if (bgEnabled) {
|
||
bgEnabled.addEventListener('change', () => this.toggleBackground());
|
||
}
|
||
|
||
// Save background button
|
||
const saveBgBtn = document.getElementById('save-bg');
|
||
if (saveBgBtn) {
|
||
saveBgBtn.addEventListener('click', () => this.saveBackgroundSettings());
|
||
}
|
||
|
||
// Remove background button
|
||
const removeBgBtn = document.getElementById('remove-bg');
|
||
if (removeBgBtn) {
|
||
removeBgBtn.addEventListener('click', () => this.removeBackground());
|
||
}
|
||
|
||
// Load saved background settings
|
||
this.loadBackgroundSettings();
|
||
}
|
||
|
||
// ============================================
|
||
// Background Functions
|
||
// ============================================
|
||
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// Timer Functions
|
||
// ============================================
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// Data Export/Import
|
||
// ============================================
|
||
|
||
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) {
|
||
// Merge or replace data
|
||
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 {
|
||
// Merge data
|
||
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');
|
||
|
||
// Set action type
|
||
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 = [];
|
||
|
||
// Clear localStorage
|
||
localStorage.removeItem(STORAGE_KEY_TEAS);
|
||
localStorage.removeItem(STORAGE_KEY_ENTRIES);
|
||
|
||
// Clear file storage if available
|
||
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();
|
||
}
|
||
|
||
// ============================================
|
||
// Tab Navigation
|
||
// ============================================
|
||
|
||
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 = '-';
|
||
}
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// Dashboard
|
||
// ============================================
|
||
|
||
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('');
|
||
}
|
||
|
||
// ============================================
|
||
// Tea List
|
||
// ============================================
|
||
|
||
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>
|
||
`;
|
||
// Add event listener for the add button
|
||
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('');
|
||
}
|
||
|
||
// ============================================
|
||
// Tea Modal
|
||
// ============================================
|
||
|
||
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 {
|
||
// New tea
|
||
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 || '';
|
||
|
||
// Get rating from star rating
|
||
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) {
|
||
// Update existing tea
|
||
this.updateTea(id, teaData);
|
||
this.showToast('Tee-Sorte erfolgreich aktualisiert!', 'success');
|
||
} else {
|
||
// Add new tea
|
||
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;
|
||
}
|
||
|
||
// ============================================
|
||
// Track Form
|
||
// ============================================
|
||
|
||
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('');
|
||
}
|
||
|
||
// ============================================
|
||
// Charts
|
||
// ============================================
|
||
|
||
initCharts() {
|
||
// Check if Chart.js is loaded
|
||
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);
|
||
|
||
// Define colors for each tea type
|
||
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);
|
||
|
||
// Format dates for display
|
||
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' : ''}`;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// ============================================
|
||
// Statistics
|
||
// ============================================
|
||
|
||
updateStats() {
|
||
const period = document.getElementById('stats-period')?.value || 'all';
|
||
this.renderDetailedStats(period);
|
||
|
||
// Update charts
|
||
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;
|
||
}
|
||
|
||
// ============================================
|
||
// Toast Notifications
|
||
// ============================================
|
||
|
||
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();
|
||
});
|