WeightTracker/js/dataManager.js
2025-05-31 07:16:31 +02:00

436 lines
16 KiB
JavaScript

/**
* 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 by fetching from API...');
const response = await fetch('/app-data/'); // Nginx proxies this to Node API
if (response.ok) {
const data = await response.json();
if (data && typeof data === 'object') {
// Merge loaded data with defaults to ensure all keys are present
appData = {
...defaultData, // Start with defaults
...data, // Override with loaded data
// Ensure critical arrays exist if not in loaded data or if they are not arrays
weights: Array.isArray(data.weights) ? data.weights : defaultData.weights,
meals: Array.isArray(data.meals) ? data.meals : defaultData.meals,
version: data.version || defaultData.version // Ensure version is present
};
console.log('Successfully loaded and merged data from API.');
} else {
console.warn('Data from API was not valid JSON or empty. Using default data.');
appData = { ...defaultData };
}
} else {
console.error(`Failed to fetch data from API: ${response.status} ${response.statusText}. Using default data.`);
appData = { ...defaultData };
}
} catch (error) {
console.error('Error initializing data via API:', error);
appData = { ...defaultData }; // Fallback to default data on critical 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 API.
*/
const saveData = async () => {
console.log('Attempting to save data via API...');
try {
const response = await fetch('/app-data/', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(appData),
});
if (response.ok) {
const result = await response.json();
console.log('Data saved successfully to API:', result.message);
// Optionally, update localStorage after successful API save
// saveDataToLocalStorage();
return true;
} else {
const errorResult = await response.json();
console.error(`Failed to save data to API: ${response.status} ${response.statusText}`, errorResult.message);
// Consider how to inform the user: alert, UI message, etc.
alert(`Failed to save data: ${errorResult.message || response.statusText}`);
return false;
}
} catch (error) {
console.error('Error saving data via API:', error);
alert(`Error saving data: ${error.message}`);
return false;
}
};
/**
* 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;
};
/**
* Get a specific meal entry by date
* @param {string} dateString - Date in YYYY-MM-DD format
* @returns {Object|undefined} - Meal entry or undefined if not found
*/
const getMealByDate = (dateString) => {
return appData.meals.find(meal => meal.date === dateString);
};
/**
* 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,
getMealByDate,
exportData,
importData,
exportCSV,
getData: () => appData
};
})();