479 lines
17 KiB
JavaScript
479 lines
17 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 = () => {
|
|
// Always return true for now since we're using Docker with Coolify
|
|
return true;
|
|
};
|
|
|
|
// API endpoint path for data operations - this must match the API endpoint in data-api.js
|
|
// Note: This is NOT a file system path, but an API endpoint URL path
|
|
const serverDataPath = '/data/weight-tracker-data.json';
|
|
|
|
/**
|
|
* Initialize data - load from server if in Docker, otherwise use localStorage
|
|
*/
|
|
const init = async () => {
|
|
try {
|
|
console.log('Initializing data manager...');
|
|
console.log('Docker environment detected:', isDockerEnvironment());
|
|
|
|
if (isDockerEnvironment()) {
|
|
// Try to load from server-side storage
|
|
try {
|
|
console.log('Attempting to load data from server at:', serverDataPath);
|
|
const response = await fetch(serverDataPath);
|
|
console.log('Server response status:', response.status, response.statusText);
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
console.log('Data received from server:', {
|
|
hasData: !!data,
|
|
hasWeights: data && !!data.weights,
|
|
weightCount: data && data.weights ? data.weights.length : 0,
|
|
hasMeals: data && !!data.meals,
|
|
mealCount: data && data.meals ? data.meals.length : 0
|
|
});
|
|
appData = data;
|
|
console.log('Data loaded from server storage successfully');
|
|
} else {
|
|
console.log('No server data found or error response. Starting with empty data.');
|
|
appData = {...defaultData};
|
|
console.log('Saving default data to server...');
|
|
await saveData(); // Save default data to server
|
|
}
|
|
} catch (serverError) {
|
|
console.warn('Exception loading from server:', serverError);
|
|
console.log('Falling back to localStorage');
|
|
loadFromLocalStorage();
|
|
}
|
|
} else {
|
|
// Use localStorage in development environment
|
|
console.log('Using localStorage in development environment');
|
|
loadFromLocalStorage();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error initializing data:', error);
|
|
appData = {...defaultData};
|
|
console.log('Saving default data due to initialization error');
|
|
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 {
|
|
console.log('Attempting to save data to server at:', serverDataPath);
|
|
console.log('Data to save:', {
|
|
weights: appData.weights.length,
|
|
meals: appData.meals.length,
|
|
version: appData.version
|
|
});
|
|
|
|
const response = await fetch(serverDataPath, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(appData)
|
|
});
|
|
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
console.log('Server response:', result);
|
|
console.log('Data saved to server storage successfully');
|
|
return true;
|
|
} else {
|
|
console.error('Error saving data to server. Status:', response.status, response.statusText);
|
|
try {
|
|
const errorData = await response.text();
|
|
console.error('Error details:', errorData);
|
|
} catch (e) {
|
|
console.error('Could not parse error response');
|
|
}
|
|
console.log('Falling back to localStorage');
|
|
return saveDataToLocalStorage(); // Fallback to localStorage
|
|
}
|
|
} catch (error) {
|
|
console.error('Exception while saving data to server:', error);
|
|
console.log('Falling back to localStorage');
|
|
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
|
|
};
|
|
})();
|