38671-vm/assets/js/main.js
2026-02-21 16:37:23 +00:00

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();
}
});
});
});