feat: add multiple goals. by Claude
This commit is contained in:
+43
-25
@@ -374,17 +374,16 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>⚙️ Settings</h1>
|
<h1>⚙️ Settings</h1>
|
||||||
|
|
||||||
<div class="date-display">Define your daily macros goal</div>
|
<div class="date-display">Body Metrics</div>
|
||||||
|
|
||||||
<form id="settingsForm" class="settings-form">
|
<form id="settingsForm" class="settings-form">
|
||||||
<!-- new - start -->
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="bodyWeight">Body Weight (kg)</label>
|
<label for="bodyWeight">Body Weight (kg)</label>
|
||||||
<input type="text" id="bodyWeight" placeholder="Body Weight" required>
|
<input type="text" id="bodyWeight" placeholder="Body Weight">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="height">Height (cm)</label>
|
<label for="height">Height (cm)</label>
|
||||||
<input type="number" id="height" placeholder="Height" required>
|
<input type="number" id="height" placeholder="Height">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="bmi">Body Mass Index (BMI)</label>
|
<label for="bmi">Body Mass Index (BMI)</label>
|
||||||
@@ -394,30 +393,49 @@
|
|||||||
<label for="bmiResult">Body Mass Index (BMI) Result</label>
|
<label for="bmiResult">Body Mass Index (BMI) Result</label>
|
||||||
<input type="text" id="bmiResult" placeholder="BMI Result" readonly>
|
<input type="text" id="bmiResult" placeholder="BMI Result" readonly>
|
||||||
</div>
|
</div>
|
||||||
<!-- new - end -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="caloriesGoal">Daily calorie goal</label>
|
|
||||||
<input type="number" id="caloriesGoal" placeholder="Calories" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="proteinGoal">Daily protein goal</label>
|
|
||||||
<input type="number" id="proteinGoal" placeholder="Proteins in grams" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="fatGoal">Daily fat goal</label>
|
|
||||||
<input type="number" id="fatGoal" placeholder="Fat in grams" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="carboGoal">Daily carbs goal</label>
|
|
||||||
<input type="number" id="carboGoal" placeholder="Carbo in grams" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="fiberGoal">Daily fiber goal</label>
|
|
||||||
<input type="number" id="fiberGoal" placeholder="Fiber in grams" required>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="settings-btn">Save</button>
|
<button type="submit" class="settings-btn">Save</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div class="date-display" style="margin-top: 40px;">Nutrition Goals</div>
|
||||||
|
|
||||||
|
<div id="goalsContainer">
|
||||||
|
<!-- Dynamically rendered list of goals -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" id="addGoalBtn" class="settings-btn" style="margin-top: 15px;">+ Add Goal</button>
|
||||||
|
|
||||||
|
<div id="goalFormContainer" class="hidden">
|
||||||
|
<form id="goalForm" class="settings-form" style="margin-top: 15px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="goalNameInput">Goal Name</label>
|
||||||
|
<input type="text" id="goalNameInput" placeholder="e.g. Workout Day" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="goalCaloriesInput">Daily calorie goal</label>
|
||||||
|
<input type="number" id="goalCaloriesInput" placeholder="Calories">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="goalProteinInput">Daily protein goal (g)</label>
|
||||||
|
<input type="number" id="goalProteinInput" placeholder="Proteins in grams">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="goalFatInput">Daily fat goal (g)</label>
|
||||||
|
<input type="number" id="goalFatInput" placeholder="Fat in grams">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="goalCarboInput">Daily carbs goal (g)</label>
|
||||||
|
<input type="number" id="goalCarboInput" placeholder="Carbs in grams">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="goalFiberInput">Daily fiber goal (g)</label>
|
||||||
|
<input type="number" id="goalFiberInput" placeholder="Fiber in grams">
|
||||||
|
</div>
|
||||||
|
<input type="hidden" id="editingGoalId" value="">
|
||||||
|
<button type="submit" class="settings-btn">Save Goal</button>
|
||||||
|
<button type="button" id="cancelGoalBtn" class="settings-btn" style="margin-top: 10px; background: #555; border-color: #555;">Cancel</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="date-display" style="margin-top: 40px;">AI Nutrition Cache</div>
|
<div class="date-display" style="margin-top: 40px;">AI Nutrition Cache</div>
|
||||||
|
|
||||||
<div class="cache-info-section">
|
<div class="cache-info-section">
|
||||||
|
|||||||
@@ -98,3 +98,98 @@
|
|||||||
background: #c45a5a;
|
background: #c45a5a;
|
||||||
border: 1px solid #c45a5a;
|
border: 1px solid #c45a5a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-goals-msg {
|
||||||
|
color: #888;
|
||||||
|
padding: 15px 0;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-card {
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 15px 20px;
|
||||||
|
background: rgba(40, 40, 40, 0.9);
|
||||||
|
border: 1px solid rgba(100, 100, 100, 0.3);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-card-active {
|
||||||
|
border-color: #6B8DD6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-name {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-active-badge {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #6B8DD6;
|
||||||
|
background: rgba(107, 141, 214, 0.15);
|
||||||
|
border: 1px solid #6B8DD6;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-summary {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #b0b0b0;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-summary span {
|
||||||
|
background: rgba(60, 60, 60, 0.5);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-actions .btn {
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid #555;
|
||||||
|
background: #333;
|
||||||
|
color: #e0e0e0;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-actions .btn:hover {
|
||||||
|
background: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-actions .goal-delete-btn {
|
||||||
|
border-color: #d66b6b;
|
||||||
|
color: #d66b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-actions .goal-delete-btn:hover {
|
||||||
|
background: rgba(214, 107, 107, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-actions .goal-select-btn {
|
||||||
|
border-color: #6B8DD6;
|
||||||
|
color: #6B8DD6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-actions .goal-select-btn:hover {
|
||||||
|
background: rgba(107, 141, 214, 0.15);
|
||||||
|
}
|
||||||
|
|||||||
@@ -264,6 +264,97 @@ export class AppwriteDB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async updateUserSettings(documentId: string, settings: Partial<UserSettings>) {
|
||||||
|
try {
|
||||||
|
const response = await databases.updateDocument(
|
||||||
|
DATABASE_ID,
|
||||||
|
USER_SETTINGS_COLLECTION_ID,
|
||||||
|
documentId,
|
||||||
|
{
|
||||||
|
...settings,
|
||||||
|
userId: (await account.get()).$id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.debug('User settings updated:', response);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Update user settings error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async saveUserGoal(goal: UserSettings) {
|
||||||
|
try {
|
||||||
|
const response = await databases.createDocument(
|
||||||
|
DATABASE_ID,
|
||||||
|
USER_SETTINGS_COLLECTION_ID,
|
||||||
|
'unique()',
|
||||||
|
{
|
||||||
|
...goal,
|
||||||
|
userId: (await account.get()).$id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.debug('User goal saved:', response);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Save user goal error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async updateUserGoal(documentId: string, goal: UserSettings) {
|
||||||
|
try {
|
||||||
|
const response = await databases.updateDocument(
|
||||||
|
DATABASE_ID,
|
||||||
|
USER_SETTINGS_COLLECTION_ID,
|
||||||
|
documentId,
|
||||||
|
{
|
||||||
|
...goal,
|
||||||
|
userId: (await account.get()).$id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.debug('User goal updated:', response);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Update user goal error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async deleteUserGoal(documentId: string) {
|
||||||
|
try {
|
||||||
|
await databases.deleteDocument(
|
||||||
|
DATABASE_ID,
|
||||||
|
USER_SETTINGS_COLLECTION_ID,
|
||||||
|
documentId
|
||||||
|
);
|
||||||
|
console.debug('User goal deleted:', documentId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Delete user goal error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async setActiveGoal(goalId: string, allGoals: UserSettings[]) {
|
||||||
|
try {
|
||||||
|
const updates = allGoals
|
||||||
|
.filter(goal => goal.id)
|
||||||
|
.map(goal =>
|
||||||
|
databases.updateDocument(
|
||||||
|
DATABASE_ID,
|
||||||
|
USER_SETTINGS_COLLECTION_ID,
|
||||||
|
goal.id!,
|
||||||
|
{ isActive: goal.id === goalId }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await Promise.all(updates);
|
||||||
|
console.debug('Active goal set:', goalId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Set active goal error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Delete food entry
|
// Delete food entry
|
||||||
static async deleteFoodEntry(documentId: string) {
|
static async deleteFoodEntry(documentId: string) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
+280
-59
@@ -1,6 +1,6 @@
|
|||||||
import { foodDatabase } from './foodDatabase.js';
|
import { foodDatabase } from './foodDatabase.js';
|
||||||
import { getButtonById, getButtonListByClassName, getDivById, getInputById, showFoodPreview } from './DomUtils';
|
import { getButtonById, getButtonListByClassName, getDivById, getInputById, showFoodPreview } from './DomUtils';
|
||||||
import { DailyTotalCalories, FoodItem, FoodStorage, MealGroup, MealPeriod } from './types.js';
|
import { DailyTotalCalories, FoodItem, FoodStorage, MealGroup, MealPeriod, UserSettings } from './types.js';
|
||||||
import { AppwriteAuth, AppwriteDB } from './appwrite.js';
|
import { AppwriteAuth, AppwriteDB } from './appwrite.js';
|
||||||
import swal from 'sweetalert';
|
import swal from 'sweetalert';
|
||||||
import { closeMobileMenu, delay, getCleanName, getIcon, handleMobileCalendarClick, hideLoading, hideSearchResults, navigateResultsKeyboard, QUICK_DELAY, scrollToCalendarView, showLoading, toggleCardHandler, toggleMobileMenu } from './Utils';
|
import { closeMobileMenu, delay, getCleanName, getIcon, handleMobileCalendarClick, hideLoading, hideSearchResults, navigateResultsKeyboard, QUICK_DELAY, scrollToCalendarView, showLoading, toggleCardHandler, toggleMobileMenu } from './Utils';
|
||||||
@@ -192,34 +192,23 @@ async function handleSaveSettings(e: SubmitEvent) {
|
|||||||
const height = getInputById('height').value;
|
const height = getInputById('height').value;
|
||||||
const bmi = getInputById('bmi').value;
|
const bmi = getInputById('bmi').value;
|
||||||
const bmiResult = getInputById('bmiResult');
|
const bmiResult = getInputById('bmiResult');
|
||||||
const caloriesGoal = getInputById('caloriesGoal').value;
|
|
||||||
const proteinGoal = getInputById('proteinGoal').value;
|
|
||||||
const fatGoal = getInputById('fatGoal').value;
|
|
||||||
const carboGoal = getInputById('carboGoal').value;
|
|
||||||
const fiberGoal = getInputById('fiberGoal').value;
|
|
||||||
|
|
||||||
// Delete existing settings - keep only the new one
|
const metricsData = {
|
||||||
const documents = await AppwriteDB.getUserSettings();
|
|
||||||
const documentsToDelete: string[] = [];
|
|
||||||
for (let i=0; i<documents.length; i++) {
|
|
||||||
documentsToDelete.push(documents[i].$id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (documentsToDelete) {
|
|
||||||
await handleBulkDelete(documentsToDelete);
|
|
||||||
}
|
|
||||||
|
|
||||||
await AppwriteDB.saveUserSettings({
|
|
||||||
caloriesGoal: parseInt(caloriesGoal),
|
|
||||||
proteinGoal: parseInt(proteinGoal),
|
|
||||||
fatGoal: parseInt(fatGoal),
|
|
||||||
carboGoal: parseInt(carboGoal),
|
|
||||||
fiberGoal: parseInt(fiberGoal),
|
|
||||||
bodyWeight: bodyWeight ? parseFloat(bodyWeight) : undefined,
|
bodyWeight: bodyWeight ? parseFloat(bodyWeight) : undefined,
|
||||||
height: height ? parseFloat(height) : undefined,
|
height: height ? parseFloat(height) : undefined,
|
||||||
bmi: bmi ? parseFloat(bmi) : undefined,
|
bmi: bmi ? parseFloat(bmi) : undefined,
|
||||||
bmiResult: bmiResult ? bmiResult.value : undefined,
|
bmiResult: bmiResult ? bmiResult.value : undefined,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// Find existing global settings doc (no goalName) and update it, or create new one
|
||||||
|
const allDocs = await AppwriteDB.getUserSettings();
|
||||||
|
const globalDoc = allDocs.find((d: any) => !d.goalName);
|
||||||
|
|
||||||
|
if (globalDoc) {
|
||||||
|
await AppwriteDB.updateUserSettings(globalDoc.$id, metricsData);
|
||||||
|
} else {
|
||||||
|
await AppwriteDB.saveUserSettings(metricsData);
|
||||||
|
}
|
||||||
|
|
||||||
hideLoading();
|
hideLoading();
|
||||||
toggleSettingsView();
|
toggleSettingsView();
|
||||||
@@ -311,27 +300,39 @@ async function toggleSettingsView() {
|
|||||||
showLoading();
|
showLoading();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const settings = await AppwriteDB.getUserSettings();
|
const allDocs = await AppwriteDB.getUserSettings();
|
||||||
const hasGoals = Array.isArray(settings) && settings.length > 0;
|
const globalSettings = allDocs.find((d: any) => !d.goalName);
|
||||||
if (hasGoals) {
|
|
||||||
const index = settings.length - 1;
|
if (globalSettings) {
|
||||||
getInputById('caloriesGoal').value = settings[index].caloriesGoal;
|
getInputById('bodyWeight').value = globalSettings.bodyWeight ?? '';
|
||||||
getInputById('proteinGoal').value = settings[index].proteinGoal;
|
getInputById('height').value = globalSettings.height ?? '';
|
||||||
getInputById('fatGoal').value = settings[index].fatGoal;
|
getInputById('bmi').value = globalSettings.bmi ?? '';
|
||||||
getInputById('carboGoal').value = settings[index].carboGoal;
|
getInputById('bmiResult').value = globalSettings.bmiResult ?? '';
|
||||||
getInputById('fiberGoal').value = settings[index].fiberGoal;
|
|
||||||
getInputById('bodyWeight').value = settings[index].bodyWeight ?? 0;
|
|
||||||
getInputById('height').value = settings[index].height ?? 0;
|
|
||||||
getInputById('bmi').value = settings[index].bmi ?? 0;
|
|
||||||
getInputById('bmiResult').value = settings[index].bmiResult ?? '';
|
|
||||||
} else {
|
} else {
|
||||||
getInputById('caloriesGoal').value = '';
|
getInputById('bodyWeight').value = '';
|
||||||
getInputById('proteinGoal').value = '';
|
getInputById('height').value = '';
|
||||||
getInputById('fatGoal').value = '';
|
getInputById('bmi').value = '';
|
||||||
getInputById('carboGoal').value = '';
|
getInputById('bmiResult').value = '';
|
||||||
getInputById('fiberGoal').value = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const goals: UserSettings[] = allDocs
|
||||||
|
.filter((d: any) => !!d.goalName)
|
||||||
|
.map((d: any) => ({
|
||||||
|
id: d.$id,
|
||||||
|
goalName: d.goalName,
|
||||||
|
isActive: d.isActive,
|
||||||
|
caloriesGoal: d.caloriesGoal,
|
||||||
|
proteinGoal: d.proteinGoal,
|
||||||
|
fatGoal: d.fatGoal,
|
||||||
|
carboGoal: d.carboGoal,
|
||||||
|
fiberGoal: d.fiberGoal,
|
||||||
|
}));
|
||||||
|
renderGoalsList(goals);
|
||||||
|
|
||||||
|
// Hide goal form if open
|
||||||
|
const goalFormContainer = document.getElementById('goalFormContainer');
|
||||||
|
if (goalFormContainer) goalFormContainer.classList.add('hidden');
|
||||||
|
|
||||||
getButtonById('settingsBtn').textContent = '🔙 Back';
|
getButtonById('settingsBtn').textContent = '🔙 Back';
|
||||||
getButtonById('settingsBtnMobile').textContent = '🔙 Back';
|
getButtonById('settingsBtnMobile').textContent = '🔙 Back';
|
||||||
|
|
||||||
@@ -474,6 +475,215 @@ const previewCalories = (foodSelected?: FoodItem) => {
|
|||||||
showFoodPreview(true);
|
showFoodPreview(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Goal management ----
|
||||||
|
|
||||||
|
function renderGoalsList(goals: UserSettings[]) {
|
||||||
|
const container = document.getElementById('goalsContainer');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
if (goals.length === 0) {
|
||||||
|
container.innerHTML = '<div class="no-goals-msg">No goals yet. Create one below.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = goals.map(goal => `
|
||||||
|
<div class="goal-card ${goal.isActive ? 'goal-card-active' : ''}">
|
||||||
|
<div class="goal-card-header">
|
||||||
|
<div class="goal-name">${goal.goalName}</div>
|
||||||
|
${goal.isActive ? '<span class="goal-active-badge">✓ Active</span>' : ''}
|
||||||
|
</div>
|
||||||
|
<div class="goal-summary">
|
||||||
|
${goal.caloriesGoal ? `<span>${goal.caloriesGoal} cal</span>` : ''}
|
||||||
|
${goal.proteinGoal ? `<span>${goal.proteinGoal}g protein</span>` : ''}
|
||||||
|
${goal.fatGoal ? `<span>${goal.fatGoal}g fat</span>` : ''}
|
||||||
|
${goal.carboGoal ? `<span>${goal.carboGoal}g carbs</span>` : ''}
|
||||||
|
${goal.fiberGoal ? `<span>${goal.fiberGoal}g fiber</span>` : ''}
|
||||||
|
</div>
|
||||||
|
<div class="goal-actions">
|
||||||
|
<button class="btn goal-edit-btn" data-goal-id="${goal.id}">Edit</button>
|
||||||
|
<button class="btn goal-delete-btn" data-goal-id="${goal.id}">Delete</button>
|
||||||
|
${!goal.isActive ? `<button class="btn goal-select-btn" data-goal-id="${goal.id}">Select</button>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
document.querySelectorAll('.goal-edit-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
const goalId = (e.currentTarget as HTMLElement).getAttribute('data-goal-id');
|
||||||
|
const goal = goals.find(g => g.id === goalId);
|
||||||
|
if (goal) handleEditGoal(goal);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.goal-delete-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
const goalId = (e.currentTarget as HTMLElement).getAttribute('data-goal-id');
|
||||||
|
if (goalId) handleDeleteGoal(goalId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.goal-select-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
const goalId = (e.currentTarget as HTMLElement).getAttribute('data-goal-id');
|
||||||
|
if (goalId) handleSelectGoal(goalId, goals);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAddGoal() {
|
||||||
|
const container = document.getElementById('goalFormContainer');
|
||||||
|
if (!container) return;
|
||||||
|
container.classList.remove('hidden');
|
||||||
|
|
||||||
|
getInputById('goalNameInput').value = '';
|
||||||
|
getInputById('goalCaloriesInput').value = '';
|
||||||
|
getInputById('goalProteinInput').value = '';
|
||||||
|
getInputById('goalFatInput').value = '';
|
||||||
|
getInputById('goalCarboInput').value = '';
|
||||||
|
getInputById('goalFiberInput').value = '';
|
||||||
|
getInputById('editingGoalId').value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEditGoal(goal: UserSettings) {
|
||||||
|
const container = document.getElementById('goalFormContainer');
|
||||||
|
if (!container) return;
|
||||||
|
container.classList.remove('hidden');
|
||||||
|
|
||||||
|
getInputById('goalNameInput').value = goal.goalName ?? '';
|
||||||
|
getInputById('goalCaloriesInput').value = goal.caloriesGoal?.toString() ?? '';
|
||||||
|
getInputById('goalProteinInput').value = goal.proteinGoal?.toString() ?? '';
|
||||||
|
getInputById('goalFatInput').value = goal.fatGoal?.toString() ?? '';
|
||||||
|
getInputById('goalCarboInput').value = goal.carboGoal?.toString() ?? '';
|
||||||
|
getInputById('goalFiberInput').value = goal.fiberGoal?.toString() ?? '';
|
||||||
|
getInputById('editingGoalId').value = goal.id ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSaveGoal(e: SubmitEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
showLoading();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const goalName = getInputById('goalNameInput').value.trim();
|
||||||
|
const caloriesGoal = getInputById('goalCaloriesInput').value;
|
||||||
|
const proteinGoal = getInputById('goalProteinInput').value;
|
||||||
|
const fatGoal = getInputById('goalFatInput').value;
|
||||||
|
const carboGoal = getInputById('goalCarboInput').value;
|
||||||
|
const fiberGoal = getInputById('goalFiberInput').value;
|
||||||
|
const editingGoalId = getInputById('editingGoalId').value;
|
||||||
|
|
||||||
|
const goalData: UserSettings = {
|
||||||
|
goalName,
|
||||||
|
caloriesGoal: caloriesGoal ? parseInt(caloriesGoal) : undefined,
|
||||||
|
proteinGoal: proteinGoal ? parseInt(proteinGoal) : undefined,
|
||||||
|
fatGoal: fatGoal ? parseInt(fatGoal) : undefined,
|
||||||
|
carboGoal: carboGoal ? parseInt(carboGoal) : undefined,
|
||||||
|
fiberGoal: fiberGoal ? parseInt(fiberGoal) : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (editingGoalId) {
|
||||||
|
await AppwriteDB.updateUserGoal(editingGoalId, goalData);
|
||||||
|
} else {
|
||||||
|
await AppwriteDB.saveUserGoal(goalData);
|
||||||
|
}
|
||||||
|
|
||||||
|
const allDocs = await AppwriteDB.getUserSettings();
|
||||||
|
const goals: UserSettings[] = allDocs
|
||||||
|
.filter((d: any) => !!d.goalName)
|
||||||
|
.map((d: any) => ({
|
||||||
|
id: d.$id,
|
||||||
|
goalName: d.goalName,
|
||||||
|
isActive: d.isActive,
|
||||||
|
caloriesGoal: d.caloriesGoal,
|
||||||
|
proteinGoal: d.proteinGoal,
|
||||||
|
fatGoal: d.fatGoal,
|
||||||
|
carboGoal: d.carboGoal,
|
||||||
|
fiberGoal: d.fiberGoal,
|
||||||
|
}));
|
||||||
|
renderGoalsList(goals);
|
||||||
|
|
||||||
|
const container = document.getElementById('goalFormContainer');
|
||||||
|
if (container) container.classList.add('hidden');
|
||||||
|
|
||||||
|
hideLoading();
|
||||||
|
} catch (error) {
|
||||||
|
hideLoading();
|
||||||
|
console.error('Save goal error:', error);
|
||||||
|
if (error instanceof Error) {
|
||||||
|
swal('Oh no!', 'Saving goal failed: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeleteGoal(id: string) {
|
||||||
|
const willDelete = await swal({
|
||||||
|
title: 'Delete Goal?',
|
||||||
|
text: 'Are you sure you want to delete this goal?',
|
||||||
|
icon: 'warning',
|
||||||
|
dangerMode: true,
|
||||||
|
buttons: ['Cancel', 'Delete'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!willDelete) return;
|
||||||
|
|
||||||
|
showLoading();
|
||||||
|
try {
|
||||||
|
await AppwriteDB.deleteUserGoal(id);
|
||||||
|
|
||||||
|
const allDocs = await AppwriteDB.getUserSettings();
|
||||||
|
const goals: UserSettings[] = allDocs
|
||||||
|
.filter((d: any) => !!d.goalName)
|
||||||
|
.map((d: any) => ({
|
||||||
|
id: d.$id,
|
||||||
|
goalName: d.goalName,
|
||||||
|
isActive: d.isActive,
|
||||||
|
caloriesGoal: d.caloriesGoal,
|
||||||
|
proteinGoal: d.proteinGoal,
|
||||||
|
fatGoal: d.fatGoal,
|
||||||
|
carboGoal: d.carboGoal,
|
||||||
|
fiberGoal: d.fiberGoal,
|
||||||
|
}));
|
||||||
|
renderGoalsList(goals);
|
||||||
|
hideLoading();
|
||||||
|
} catch (error) {
|
||||||
|
hideLoading();
|
||||||
|
console.error('Delete goal error:', error);
|
||||||
|
if (error instanceof Error) {
|
||||||
|
swal('Oh no!', 'Deleting goal failed: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSelectGoal(goalId: string, allGoals: UserSettings[]) {
|
||||||
|
showLoading();
|
||||||
|
try {
|
||||||
|
await AppwriteDB.setActiveGoal(goalId, allGoals);
|
||||||
|
|
||||||
|
const allDocs = await AppwriteDB.getUserSettings();
|
||||||
|
const goals: UserSettings[] = allDocs
|
||||||
|
.filter((d: any) => !!d.goalName)
|
||||||
|
.map((d: any) => ({
|
||||||
|
id: d.$id,
|
||||||
|
goalName: d.goalName,
|
||||||
|
isActive: d.isActive,
|
||||||
|
caloriesGoal: d.caloriesGoal,
|
||||||
|
proteinGoal: d.proteinGoal,
|
||||||
|
fatGoal: d.fatGoal,
|
||||||
|
carboGoal: d.carboGoal,
|
||||||
|
fiberGoal: d.fiberGoal,
|
||||||
|
}));
|
||||||
|
renderGoalsList(goals);
|
||||||
|
hideLoading();
|
||||||
|
} catch (error) {
|
||||||
|
hideLoading();
|
||||||
|
console.error('Select goal error:', error);
|
||||||
|
if (error instanceof Error) {
|
||||||
|
swal('Oh no!', 'Selecting goal failed: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- End goal management ----
|
||||||
|
|
||||||
const setupEventListeners = () => {
|
const setupEventListeners = () => {
|
||||||
getInputById('foodSearchInput').addEventListener('change', (e: Event) => {
|
getInputById('foodSearchInput').addEventListener('change', (e: Event) => {
|
||||||
const target = e.target as HTMLInputElement;
|
const target = e.target as HTMLInputElement;
|
||||||
@@ -549,6 +759,14 @@ const setupEventListeners = () => {
|
|||||||
// Clear cache button
|
// Clear cache button
|
||||||
getButtonById('clearCacheBtn')?.addEventListener('click', handleClearCache);
|
getButtonById('clearCacheBtn')?.addEventListener('click', handleClearCache);
|
||||||
|
|
||||||
|
// Goal management buttons
|
||||||
|
document.getElementById('addGoalBtn')?.addEventListener('click', handleAddGoal);
|
||||||
|
document.getElementById('cancelGoalBtn')?.addEventListener('click', () => {
|
||||||
|
const container = document.getElementById('goalFormContainer');
|
||||||
|
if (container) container.classList.add('hidden');
|
||||||
|
});
|
||||||
|
document.getElementById('goalForm')?.addEventListener('submit', handleSaveGoal);
|
||||||
|
|
||||||
// AI nutrition checkbox
|
// AI nutrition checkbox
|
||||||
document.getElementById('enableAINutrition')?.addEventListener('change', handleAICheckboxChange);
|
document.getElementById('enableAINutrition')?.addEventListener('change', handleAICheckboxChange);
|
||||||
|
|
||||||
@@ -1363,51 +1581,54 @@ async function loadFoodEntries(date: Date) {
|
|||||||
getDivById('fiberValue').textContent = (Math.round(totalFiber * 10) / 10).toString();
|
getDivById('fiberValue').textContent = (Math.round(totalFiber * 10) / 10).toString();
|
||||||
|
|
||||||
// Update goals
|
// Update goals
|
||||||
const settings = await AppwriteDB.getUserSettings();
|
const allDocs = await AppwriteDB.getUserSettings();
|
||||||
const hasGoals = Array.isArray(settings) && settings.length > 0;
|
const globalSettings = allDocs.find((d: any) => !d.goalName);
|
||||||
if (hasGoals) {
|
const activeGoal = allDocs.find((d: any) => d.goalName && d.isActive);
|
||||||
const index = settings.length - 1;
|
|
||||||
const bodyWeight = parseInt(settings[index].bodyWeight);
|
if (activeGoal) {
|
||||||
|
const bodyWeight = globalSettings ? parseInt(globalSettings.bodyWeight) : 0;
|
||||||
|
|
||||||
getDivById('caloriesGoalText').classList.add('hidden');
|
getDivById('caloriesGoalText').classList.add('hidden');
|
||||||
if (parseInt(settings[index].caloriesGoal) > 0) {
|
if (activeGoal.caloriesGoal && parseInt(activeGoal.caloriesGoal) > 0) {
|
||||||
getDivById('caloriesGoalText').textContent = `of ${settings[index].caloriesGoal}`;
|
getDivById('caloriesGoalText').textContent = `of ${activeGoal.caloriesGoal}`;
|
||||||
getDivById('caloriesGoalText').classList.remove('hidden');
|
getDivById('caloriesGoalText').classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
getDivById('proteinGoalText').classList.add('hidden');
|
getDivById('proteinGoalText').classList.add('hidden');
|
||||||
if (parseInt(settings[index].proteinGoal) > 0) {
|
if (activeGoal.proteinGoal && parseInt(activeGoal.proteinGoal) > 0) {
|
||||||
let percent = '';
|
let percent = '';
|
||||||
if (bodyWeight > 0) {
|
if (bodyWeight > 0) {
|
||||||
const proteinPerKg = Math.round((parseInt(settings[index].proteinGoal) / bodyWeight) * 10) / 10;
|
const proteinPerKg = Math.round((parseInt(activeGoal.proteinGoal) / bodyWeight) * 10) / 10;
|
||||||
percent = ` (${proteinPerKg} g/kg)`;
|
percent = ` (${proteinPerKg} g/kg)`;
|
||||||
}
|
}
|
||||||
getDivById('proteinGoalText').textContent = `of ${settings[index].proteinGoal}${percent}`;
|
getDivById('proteinGoalText').textContent = `of ${activeGoal.proteinGoal}${percent}`;
|
||||||
getDivById('proteinGoalText').classList.remove('hidden');
|
getDivById('proteinGoalText').classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
getDivById('fatGoalText').classList.add('hidden');
|
getDivById('fatGoalText').classList.add('hidden');
|
||||||
if (parseInt(settings[index].fatGoal) > 0) {
|
if (activeGoal.fatGoal && parseInt(activeGoal.fatGoal) > 0) {
|
||||||
getDivById('fatGoalText').textContent = `of ${settings[index].fatGoal}`;
|
getDivById('fatGoalText').textContent = `of ${activeGoal.fatGoal}`;
|
||||||
getDivById('fatGoalText').classList.remove('hidden');
|
getDivById('fatGoalText').classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
getDivById('carboGoalText').classList.add('hidden');
|
getDivById('carboGoalText').classList.add('hidden');
|
||||||
if (parseInt(settings[index].carboGoal) > 0) {
|
if (activeGoal.carboGoal && parseInt(activeGoal.carboGoal) > 0) {
|
||||||
let percent = '';
|
let percent = '';
|
||||||
if (bodyWeight > 0) {
|
if (bodyWeight > 0) {
|
||||||
const carboPerKg = Math.round((parseInt(settings[index].carboGoal) / bodyWeight) * 10) / 10;
|
const carboPerKg = Math.round((parseInt(activeGoal.carboGoal) / bodyWeight) * 10) / 10;
|
||||||
percent = ` (${carboPerKg} g/kg)`;
|
percent = ` (${carboPerKg} g/kg)`;
|
||||||
}
|
}
|
||||||
getDivById('carboGoalText').textContent = `of ${settings[index].carboGoal}${percent}`;
|
getDivById('carboGoalText').textContent = `of ${activeGoal.carboGoal}${percent}`;
|
||||||
getDivById('carboGoalText').classList.remove('hidden');
|
getDivById('carboGoalText').classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
getDivById('fiberGoalText').classList.add('hidden');
|
getDivById('fiberGoalText').classList.add('hidden');
|
||||||
if (parseInt(settings[index].fiberGoal) > 0) {
|
if (activeGoal.fiberGoal && parseInt(activeGoal.fiberGoal) > 0) {
|
||||||
getDivById('fiberGoalText').textContent = `of ${settings[index].fiberGoal}`;
|
getDivById('fiberGoalText').textContent = `of ${activeGoal.fiberGoal}`;
|
||||||
getDivById('fiberGoalText').classList.remove('hidden');
|
getDivById('fiberGoalText').classList.remove('hidden');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
getDivById('caloriesGoalText').classList.add('hidden');
|
||||||
getDivById('proteinGoalText').classList.add('hidden');
|
getDivById('proteinGoalText').classList.add('hidden');
|
||||||
getDivById('fatGoalText').classList.add('hidden');
|
getDivById('fatGoalText').classList.add('hidden');
|
||||||
getDivById('carboGoalText').classList.add('hidden');
|
getDivById('carboGoalText').classList.add('hidden');
|
||||||
|
|||||||
+6
-4
@@ -23,14 +23,16 @@ export type FoodStorage = {
|
|||||||
export type UserSettings = {
|
export type UserSettings = {
|
||||||
id?: string;
|
id?: string;
|
||||||
caloriesGoal?: number;
|
caloriesGoal?: number;
|
||||||
proteinGoal: number;
|
proteinGoal?: number;
|
||||||
fatGoal: number;
|
fatGoal?: number;
|
||||||
carboGoal: number;
|
carboGoal?: number;
|
||||||
fiberGoal: number;
|
fiberGoal?: number;
|
||||||
bodyWeight?: number;
|
bodyWeight?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
bmi?: number;
|
bmi?: number;
|
||||||
bmiResult?: string;
|
bmiResult?: string;
|
||||||
|
goalName?: string;
|
||||||
|
isActive?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DailyTotalCalories = {
|
export type DailyTotalCalories = {
|
||||||
|
|||||||
Reference in New Issue
Block a user