384 lines
15 KiB
JavaScript
384 lines
15 KiB
JavaScript
document.addEventListener('DOMContentLoaded', function() {
|
|
// Elements
|
|
const statsContainer = document.getElementById('stats-container');
|
|
const logsList = document.getElementById('recent-logs-list');
|
|
const addLogForm = document.getElementById('addLogForm');
|
|
const updateGoalsForm = document.getElementById('updateGoalsForm');
|
|
const weightForm = document.getElementById('weightForm');
|
|
|
|
// AI Elements
|
|
const btnAnalyzeAI = document.getElementById('btnAnalyzeAI');
|
|
const aiInput = document.getElementById('aiInput');
|
|
const aiFeedback = document.getElementById('aiFeedback');
|
|
const aiBtnText = document.getElementById('aiBtnText');
|
|
const aiBtnSpinner = document.getElementById('aiBtnSpinner');
|
|
const manualTab = document.getElementById('manual-tab');
|
|
|
|
// Tab Elements
|
|
const analysisTab = document.getElementById('analysis-tab');
|
|
const periodWeekly = document.getElementById('periodWeekly');
|
|
const periodMonthly = document.getElementById('periodMonthly');
|
|
|
|
let weightChart = null;
|
|
let caloriesChart = null;
|
|
|
|
// Initial fetch
|
|
refreshAll();
|
|
|
|
function refreshAll() {
|
|
fetchStats();
|
|
fetchRecentLogs();
|
|
fetchWater();
|
|
fetchWeight();
|
|
}
|
|
|
|
// --- NUTRITION & STATS ---
|
|
function fetchStats() {
|
|
fetch('api/logs.php?action=get_stats')
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
const { goals, consumed, remaining } = data;
|
|
|
|
// Update Goals (UI)
|
|
setText('cal-goal', goals.calories);
|
|
setText('pro-goal', goals.protein);
|
|
setText('cre-goal', goals.creatine || 0);
|
|
|
|
// Update Consumed (UI)
|
|
setText('cal-consumed', consumed.calories);
|
|
setText('pro-consumed', consumed.protein);
|
|
setText('cre-consumed', consumed.creatine || 0);
|
|
|
|
// Update Left (UI)
|
|
setText('cal-left', Math.max(0, remaining.calories));
|
|
setText('pro-left', Math.max(0, remaining.protein));
|
|
// setText('cre-left', Math.max(0, (goals.creatine || 0) - (consumed.creatine || 0)));
|
|
|
|
// Update Progress (UI)
|
|
updateProgress('cal-progress', consumed.calories, goals.calories);
|
|
updateProgress('pro-progress', consumed.protein, goals.protein);
|
|
|
|
// Supplements status
|
|
const creGoal = parseFloat(goals.creatine || 0);
|
|
const creCons = parseFloat(consumed.creatine || 0);
|
|
updateProgress('cre-progress-health', creCons, creGoal);
|
|
const creBadge = document.getElementById('cre-status');
|
|
if (creCons >= creGoal && creGoal > 0) {
|
|
creBadge.innerText = 'Taken';
|
|
creBadge.className = 'badge bg-success';
|
|
} else {
|
|
creBadge.innerText = 'Not taken';
|
|
creBadge.className = 'badge bg-danger';
|
|
}
|
|
|
|
// Pre-fill goal form
|
|
document.getElementById('goalCalories').value = goals.calories;
|
|
document.getElementById('goalProtein').value = goals.protein;
|
|
document.getElementById('goalCreatine').value = goals.creatine;
|
|
document.getElementById('goalWater').value = goals.water || 2.5;
|
|
|
|
updateReminders(consumed, goals);
|
|
});
|
|
}
|
|
|
|
function updateProgress(id, consumed, goal) {
|
|
const el = document.getElementById(id);
|
|
if (!el) return;
|
|
const percent = goal > 0 ? Math.min(100, (consumed / goal) * 100) : 0;
|
|
el.style.width = percent + '%';
|
|
if (percent >= 100) {
|
|
el.style.backgroundColor = 'var(--success)';
|
|
} else {
|
|
// Keep original if not success (some bars have specific colors)
|
|
if (!el.classList.contains('bg-info')) {
|
|
el.style.backgroundColor = 'var(--primary)';
|
|
}
|
|
}
|
|
}
|
|
|
|
function setText(id, val) {
|
|
const el = document.getElementById(id);
|
|
if (el) el.innerText = val;
|
|
}
|
|
|
|
function fetchRecentLogs() {
|
|
fetch('api/logs.php?action=get_recent')
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.length === 0) {
|
|
logsList.innerHTML = '<div class="text-center py-4"><p class="text-muted small mb-0">No entries today yet.</p></div>';
|
|
return;
|
|
}
|
|
logsList.innerHTML = data.map(log => `
|
|
<div class="log-item">
|
|
<div>
|
|
<div class="fw-bold mb-1">${log.entry_name}</div>
|
|
<div class="log-details">${log.calories} kcal · ${log.protein}g protein ${log.creatine > 0 ? '· ' + log.creatine + 'g creatine' : ''}</div>
|
|
</div>
|
|
<div class="text-muted small">${new Date(log.created_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</div>
|
|
</div>
|
|
`).join('');
|
|
});
|
|
}
|
|
|
|
// --- WATER ---
|
|
function fetchWater() {
|
|
fetch('api/water.php')
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
const amount = data.amount || 0;
|
|
setText('water-consumed', amount.toFixed(2));
|
|
fetch('api/logs.php?action=get_stats')
|
|
.then(res => res.json())
|
|
.then(stats => {
|
|
const goal = stats.goals.water || 2.5;
|
|
setText('water-goal', goal);
|
|
updateProgress('water-progress', amount, goal);
|
|
});
|
|
});
|
|
}
|
|
|
|
document.querySelectorAll('.water-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const amount = parseFloat(btn.dataset.amount);
|
|
logWater(amount);
|
|
});
|
|
});
|
|
|
|
document.getElementById('btnAddWater').addEventListener('click', () => {
|
|
logWater(0.25); // Default add 250ml
|
|
});
|
|
|
|
function logWater(amount) {
|
|
fetch('api/water.php', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ amount: amount })
|
|
}).then(() => fetchWater());
|
|
}
|
|
|
|
// --- WEIGHT ---
|
|
function fetchWeight() {
|
|
fetch('api/weight.php')
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.length > 0) {
|
|
const last = data[0];
|
|
setText('weight-current', last.weight);
|
|
setText('weight-last-date', 'Last logged: ' + last.logged_at);
|
|
document.getElementById('weightInput').value = last.weight;
|
|
}
|
|
});
|
|
}
|
|
|
|
weightForm.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const weight = document.getElementById('weightInput').value;
|
|
fetch('api/weight.php', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ weight: weight })
|
|
}).then(res => res.json())
|
|
.then(res => {
|
|
if (res.success) {
|
|
bootstrap.Modal.getInstance(document.getElementById('weightModal')).hide();
|
|
fetchWeight();
|
|
}
|
|
});
|
|
});
|
|
|
|
// --- REMINDERS ---
|
|
function updateReminders(consumed, goals) {
|
|
const row = document.getElementById('reminders-row');
|
|
row.innerHTML = '';
|
|
|
|
const items = [
|
|
{ label: 'Water', current: consumed.water || 0, goal: goals.water || 2.5, unit: 'L', color: 'info' },
|
|
{ label: 'Protein', current: consumed.protein, goal: goals.protein, unit: 'g', color: 'primary' },
|
|
{ label: 'Creatine', current: consumed.creatine || 0, goal: goals.creatine || 5, unit: 'g', color: 'warning' }
|
|
];
|
|
|
|
// Fetch water separately for reminders since it's not in get_stats usually
|
|
fetch('api/water.php').then(res => res.json()).then(waterData => {
|
|
items[0].current = waterData.amount || 0;
|
|
|
|
items.forEach(item => {
|
|
const done = item.current >= item.goal && item.goal > 0;
|
|
const div = document.createElement('div');
|
|
div.className = `badge ${done ? 'bg-success-subtle text-success' : 'bg-secondary-subtle text-muted'} border border-opacity-10 px-3 py-2 rounded-pill flex-shrink-0 d-flex align-items-center gap-2`;
|
|
div.innerHTML = `
|
|
<span style="width:8px; height:8px;" class="rounded-circle bg-${done ? 'success' : item.color}"></span>
|
|
<span>${item.label}: ${done ? 'Done' : (item.goal - item.current).toFixed(1) + item.unit + ' left'}</span>
|
|
`;
|
|
row.appendChild(div);
|
|
});
|
|
});
|
|
}
|
|
|
|
// --- ANALYSIS & CHARTS ---
|
|
analysisTab.addEventListener('shown.bs.tab', () => {
|
|
renderAnalysis();
|
|
});
|
|
|
|
periodWeekly.addEventListener('change', renderAnalysis);
|
|
periodMonthly.addEventListener('change', renderAnalysis);
|
|
|
|
function renderAnalysis() {
|
|
const type = periodMonthly.checked ? 'monthly' : 'weekly';
|
|
fetch(`api/analysis.php?type=${type}`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
initWeightChart(data.weight);
|
|
initCaloriesChart(data.nutrition);
|
|
});
|
|
}
|
|
|
|
function initWeightChart(weightData) {
|
|
const ctx = document.getElementById('weightChart').getContext('2d');
|
|
if (weightChart) weightChart.destroy();
|
|
|
|
weightChart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: weightData.map(d => d.logged_at),
|
|
datasets: [{
|
|
label: 'Weight (kg)',
|
|
data: weightData.map(d => d.weight),
|
|
borderColor: '#f59e0b',
|
|
backgroundColor: 'rgba(245, 158, 11, 0.1)',
|
|
fill: true,
|
|
tension: 0.4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: { legend: { display: false } },
|
|
scales: {
|
|
y: { grid: { color: '#334155' }, ticks: { color: '#94a3b8' } },
|
|
x: { grid: { display: false }, ticks: { color: '#94a3b8' } }
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function initCaloriesChart(nutritionData) {
|
|
const ctx = document.getElementById('caloriesChart').getContext('2d');
|
|
if (caloriesChart) caloriesChart.destroy();
|
|
|
|
// Get goal from stats
|
|
fetch('api/logs.php?action=get_stats').then(res => res.json()).then(stats => {
|
|
const goal = stats.goals.calories;
|
|
caloriesChart = new Chart(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: nutritionData.map(d => d.log_date),
|
|
datasets: [{
|
|
label: 'Consumed',
|
|
data: nutritionData.map(d => d.total_calories),
|
|
backgroundColor: nutritionData.map(d => d.total_calories >= goal ? '#10b981' : '#3b82f6'),
|
|
borderRadius: 4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: { display: false },
|
|
annotation: {
|
|
annotations: {
|
|
line1: {
|
|
type: 'line',
|
|
yMin: goal,
|
|
yMax: goal,
|
|
borderColor: 'rgba(255, 255, 255, 0.5)',
|
|
borderWidth: 2,
|
|
borderDash: [6, 6]
|
|
}
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
y: { grid: { color: '#334155' }, ticks: { color: '#94a3b8' } },
|
|
x: { grid: { display: false }, ticks: { color: '#94a3b8' } }
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// --- FORM HANDLERS ---
|
|
addLogForm.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const data = {
|
|
entry_name: document.getElementById('logName').value,
|
|
calories: document.getElementById('logCalories').value || 0,
|
|
protein: document.getElementById('logProtein').value || 0,
|
|
creatine: document.getElementById('logCreatine').value || 0
|
|
};
|
|
|
|
fetch('api/logs.php?action=add_log', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data)
|
|
}).then(res => res.json())
|
|
.then(res => {
|
|
if (res.success) {
|
|
bootstrap.Modal.getInstance(document.getElementById('addLogModal')).hide();
|
|
addLogForm.reset();
|
|
aiInput.value = '';
|
|
refreshAll();
|
|
}
|
|
});
|
|
});
|
|
|
|
btnAnalyzeAI.addEventListener('click', function() {
|
|
const text = aiInput.value.trim();
|
|
if (!text) return;
|
|
|
|
aiBtnText.textContent = 'Analyzing...';
|
|
aiBtnSpinner.classList.remove('d-none');
|
|
btnAnalyzeAI.disabled = true;
|
|
|
|
fetch('api/ai_analyze.php', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ text: text })
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
document.getElementById('logName').value = data.entry_name;
|
|
document.getElementById('logCalories').value = data.calories;
|
|
document.getElementById('logProtein').value = data.protein;
|
|
document.getElementById('logCreatine').value = data.creatine;
|
|
new bootstrap.Tab(document.getElementById('manual-tab')).show();
|
|
resetAIButton();
|
|
})
|
|
.catch(() => resetAIButton());
|
|
});
|
|
|
|
function resetAIButton() {
|
|
aiBtnText.textContent = 'Analyze with AI';
|
|
aiBtnSpinner.classList.add('d-none');
|
|
btnAnalyzeAI.disabled = false;
|
|
}
|
|
|
|
updateGoalsForm.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const data = {
|
|
calories: document.getElementById('goalCalories').value,
|
|
protein: document.getElementById('goalProtein').value,
|
|
creatine: document.getElementById('goalCreatine').value,
|
|
water: document.getElementById('goalWater').value,
|
|
weight: 75 // placeholder or add input if needed
|
|
};
|
|
|
|
fetch('api/logs.php?action=update_goals', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data)
|
|
}).then(res => res.json())
|
|
.then(res => {
|
|
if (res.success) {
|
|
bootstrap.Modal.getInstance(document.getElementById('goalsModal')).hide();
|
|
refreshAll();
|
|
}
|
|
});
|
|
});
|
|
});
|