WeightTracker/js/dataManager.js

444 lines
15 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};
// 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
};
})();