/** * Data Manager Module * Handles all data operations including loading, saving, importing, and exporting data * Supports both localStorage (for development) and server-side storage (for Docker deployment) */ const DataManager = (() => { // Default empty data structure const defaultData = { weights: [], meals: [], version: '1.0.0' }; // Current application data let appData = {...defaultData}; // Determine if we're running in Docker (has /data endpoint) const isDockerEnvironment = () => { // Check if we're in a deployment environment with the /data endpoint return window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1'; }; // Storage file path for Docker environment const serverDataPath = '/data/weight-tracker-data.json'; /** * Initialize data - load from server if in Docker, otherwise use localStorage */ const init = async () => { try { if (isDockerEnvironment()) { // Try to load from server-side storage try { const response = await fetch(serverDataPath); if (response.ok) { appData = await response.json(); console.log('Data loaded from server storage'); } else { console.log('No server data found. Starting with empty data.'); appData = {...defaultData}; await saveData(); // Save default data to server } } catch (serverError) { console.warn('Error loading from server, falling back to localStorage:', serverError); loadFromLocalStorage(); } } else { // Use localStorage in development environment loadFromLocalStorage(); } } catch (error) { console.error('Error initializing data:', error); appData = {...defaultData}; saveData(); // Save default data structure on error } }; /** * Load data from localStorage (used in development or as fallback) */ const loadFromLocalStorage = () => { const savedData = localStorage.getItem('weightTrackerData'); if (savedData) { appData = JSON.parse(savedData); console.log('Data loaded from localStorage'); } else { console.log('No existing data found in localStorage. Starting with empty data.'); appData = {...defaultData}; saveDataToLocalStorage(); // Save default data structure } }; /** * Save data to either server (in Docker) or localStorage (in development) */ const saveData = async () => { if (isDockerEnvironment()) { return saveDataToServer(); } else { return saveDataToLocalStorage(); } }; /** * Save data to localStorage (development environment) */ const saveDataToLocalStorage = () => { try { localStorage.setItem('weightTrackerData', JSON.stringify(appData)); console.log('Data saved to localStorage'); return true; } catch (error) { console.error('Error saving data to localStorage:', error); return false; } }; /** * Save data to server (Docker environment) */ const saveDataToServer = async () => { try { const response = await fetch(serverDataPath, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(appData) }); if (response.ok) { console.log('Data saved to server storage'); return true; } else { console.error('Error saving data to server:', response.statusText); return saveDataToLocalStorage(); // Fallback to localStorage } } catch (error) { console.error('Error saving data to server:', error); return saveDataToLocalStorage(); // Fallback to localStorage } }; /** * Add a weight entry * @param {Object} entry - Weight entry {date, weight, notes} */ const addWeight = (entry) => { // Create an entry with a unique ID const newEntry = { id: generateId(), date: entry.date, weight: parseFloat(entry.weight), notes: entry.notes || '', createdAt: new Date().toISOString() }; appData.weights.push(newEntry); appData.weights.sort((a, b) => new Date(b.date) - new Date(a.date)); // Sort by date, newest first return saveData(); }; /** * Delete a weight entry * @param {string} id - Entry ID */ const deleteWeight = (id) => { appData.weights = appData.weights.filter(entry => entry.id !== id); return saveData(); }; /** * Update a weight entry * @param {Object} entry - Updated entry data */ const updateWeight = (entry) => { const index = appData.weights.findIndex(item => item.id === entry.id); if (index !== -1) { appData.weights[index] = { ...appData.weights[index], date: entry.date, weight: parseFloat(entry.weight), notes: entry.notes || '', updatedAt: new Date().toISOString() }; appData.weights.sort((a, b) => new Date(b.date) - new Date(a.date)); return saveData(); } return false; }; /** * Get all weight entries * @param {Object} filters - Optional filters {startDate, endDate} * @returns {Array} - Filtered weight entries */ const getWeights = (filters = {}) => { let results = [...appData.weights]; if (filters.startDate) { results = results.filter(entry => new Date(entry.date) >= new Date(filters.startDate)); } if (filters.endDate) { results = results.filter(entry => new Date(entry.date) <= new Date(filters.endDate)); } return results; }; /** * Add a meal entry * @param {Object} entry - Meal entry {date, breakfast, lunch, dinner, otherMeals} */ const addMeal = (entry) => { // Check if a meal entry for this date already exists const existingIndex = appData.meals.findIndex(meal => meal.date === entry.date); if (existingIndex !== -1) { // Update existing entry appData.meals[existingIndex] = { ...appData.meals[existingIndex], breakfast: entry.breakfast || '', lunch: entry.lunch || '', dinner: entry.dinner || '', otherMeals: entry.otherMeals || '', updatedAt: new Date().toISOString() }; } else { // Create new entry const newEntry = { id: generateId(), date: entry.date, breakfast: entry.breakfast || '', lunch: entry.lunch || '', dinner: entry.dinner || '', otherMeals: entry.otherMeals || '', createdAt: new Date().toISOString() }; appData.meals.push(newEntry); } appData.meals.sort((a, b) => new Date(b.date) - new Date(a.date)); // Sort by date, newest first return saveData(); }; /** * Delete a meal entry * @param {string} id - Entry ID */ const deleteMeal = (id) => { appData.meals = appData.meals.filter(entry => entry.id !== id); return saveData(); }; /** * Get all meal entries * @param {Object} filters - Optional filters {startDate, endDate} * @returns {Array} - Filtered meal entries */ const getMeals = (filters = {}) => { let results = [...appData.meals]; if (filters.startDate) { results = results.filter(entry => new Date(entry.date) >= new Date(filters.startDate)); } if (filters.endDate) { results = results.filter(entry => new Date(entry.date) <= new Date(filters.endDate)); } return results; }; /** * Export all data as a JSON file */ const exportData = () => { try { const dataStr = JSON.stringify(appData, null, 2); // Create a blob instead of using data URI const blob = new Blob([dataStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const exportFileDefaultName = `weight-tracker-backup-${formatDateForFilename(new Date())}.json`; const linkElement = document.createElement('a'); linkElement.setAttribute('href', url); linkElement.setAttribute('download', exportFileDefaultName); linkElement.style.display = 'none'; // Add to DOM, trigger click, and clean up document.body.appendChild(linkElement); linkElement.click(); // Clean up setTimeout(() => { document.body.removeChild(linkElement); URL.revokeObjectURL(url); }, 100); return true; } catch (error) { console.error('Error exporting data:', error); return false; } }; /** * Import data from a JSON file * @param {Object} importedData - Data to import * @param {string} mode - 'merge' or 'overwrite' * @returns {boolean} - Success status */ const importData = (importedData, mode = 'merge') => { try { // Validate imported data has the required structure if (!importedData || !importedData.weights || !Array.isArray(importedData.weights) || !importedData.meals || !Array.isArray(importedData.meals)) { console.error('Invalid data structure in imported file'); return false; } if (mode === 'overwrite') { // Overwrite all data appData = { ...importedData, version: importedData.version || '1.0.0' // Ensure version exists }; } else { // Merge data - combine arrays and remove duplicates by ID const mergedWeights = [...appData.weights]; const mergedMeals = [...appData.meals]; // Process imported weights importedData.weights.forEach(importedWeight => { const existingIndex = mergedWeights.findIndex(w => w.id === importedWeight.id); if (existingIndex !== -1) { mergedWeights[existingIndex] = importedWeight; // Replace with imported } else { mergedWeights.push(importedWeight); // Add new entry } }); // Process imported meals importedData.meals.forEach(importedMeal => { const existingIndex = mergedMeals.findIndex(m => m.id === importedMeal.id); if (existingIndex !== -1) { mergedMeals[existingIndex] = importedMeal; // Replace with imported } else { mergedMeals.push(importedMeal); // Add new entry } }); // Sort arrays by date mergedWeights.sort((a, b) => new Date(b.date) - new Date(a.date)); mergedMeals.sort((a, b) => new Date(b.date) - new Date(a.date)); // Update app data appData = { weights: mergedWeights, meals: mergedMeals, version: appData.version // Keep current version }; } return saveData(); } catch (error) { console.error('Error importing data:', error); return false; } }; /** * Export data as CSV * @param {string} type - 'weights' or 'meals' */ const exportCSV = (type) => { try { let csvContent = ''; let filename = ''; if (type === 'weights') { // CSV Header csvContent = 'Date,Weight (kg),Notes\n'; // Add data rows appData.weights.forEach(entry => { const notes = entry.notes ? `"${entry.notes.replace(/"/g, '""')}"` : ''; csvContent += `${entry.date},${entry.weight},${notes}\n`; }); filename = `weight-data-${formatDateForFilename(new Date())}.csv`; } else if (type === 'meals') { // CSV Header csvContent = 'Date,Breakfast,Lunch,Dinner,Other\n'; // Add data rows appData.meals.forEach(entry => { const breakfast = entry.breakfast ? `"${entry.breakfast.replace(/"/g, '""')}"` : ''; const lunch = entry.lunch ? `"${entry.lunch.replace(/"/g, '""')}"` : ''; const dinner = entry.dinner ? `"${entry.dinner.replace(/"/g, '""')}"` : ''; const other = entry.otherMeals ? `"${entry.otherMeals.replace(/"/g, '""')}"` : ''; csvContent += `${entry.date},${breakfast},${lunch},${dinner},${other}\n`; }); filename = `meal-data-${formatDateForFilename(new Date())}.csv`; } const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); const url = URL.createObjectURL(blob); link.setAttribute('href', url); link.setAttribute('download', filename); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); } catch (error) { console.error('Error exporting CSV:', error); } }; /** * Generate a unique ID for entries * @returns {string} - Unique ID */ const generateId = () => { return Date.now().toString(36) + Math.random().toString(36).substr(2, 5); }; /** * Format date for filename * @param {Date} date - Date to format * @returns {string} - Formatted date string (YYYY-MM-DD) */ const formatDateForFilename = (date) => { return date.toISOString().split('T')[0]; }; // Return public API return { init, addWeight, deleteWeight, updateWeight, getWeights, addMeal, deleteMeal, getMeals, exportData, importData, exportCSV, getData: () => appData }; })();