/** * 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}; /** * Initialize data - load from localStorage or use defaults. * Actual file loading will be a separate user-initiated action. */ const init = async () => { try { console.log('Initializing data manager...'); loadFromLocalStorage(); // Load from localStorage or set defaults } catch (error) { console.error('Error initializing data:', error); appData = {...defaultData}; // Fallback to default data // Attempt to save default data to localStorage if init failed badly try { saveDataToLocalStorage(); } catch (e) { console.error('Failed to save default data to LS during init error handling', e); } } }; /** * 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 localStorage. * Actual file saving will be a separate user-initiated action (handled by exportData). */ const saveData = async () => { 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; } }; /** * 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 jsonData = JSON.stringify(appData, null, 2); // Pretty print JSON const blob = new Blob([jsonData], { type: 'application/json;charset=utf-8;' }); const link = document.createElement('a'); const url = URL.createObjectURL(blob); link.setAttribute('href', url); link.setAttribute('download', `weight-tracker-data-${formatDateForFilename(new Date())}.json`); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); console.log('Data exported as JSON file.'); return true; } catch (error) { console.error('Error exporting data as JSON:', 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 }; })();