Flatlogic Bot 1cb0064488 mb v1
2026-02-21 16:48:44 +00:00

485 lines
19 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');
const photoForm = document.getElementById('photoForm');
// AI Elements
const btnAnalyzeAI = document.getElementById('btnAnalyzeAI');
const aiInput = document.getElementById('aiInput');
const aiBtnText = document.getElementById('aiBtnText');
const aiBtnSpinner = document.getElementById('aiBtnSpinner');
// Tab Elements
const periodWeekly = document.getElementById('periodWeekly');
const periodMonthly = document.getElementById('periodMonthly');
let weightChart = null;
let caloriesChart = null;
// --- PWA SERVICE WORKER ---
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js').then(() => {
console.log('Service Worker Registered');
});
}
// --- BOTTOM NAV & TAB HANDLING ---
const bottomNavItems = document.querySelectorAll('.bottom-nav .nav-item');
bottomNavItems.forEach(item => {
item.addEventListener('click', function(e) {
e.preventDefault();
const tabId = this.dataset.tab;
// Update UI
bottomNavItems.forEach(i => i.classList.remove('active'));
this.classList.add('active');
// Show Tab
const tabTrigger = new bootstrap.Tab(document.querySelector(`#${tabId}-tab`));
tabTrigger.show();
// Special handling for specific tabs
if (tabId === 'gallery') fetchPhotos();
if (tabId === 'analysis') renderAnalysis();
// Scroll to top
window.scrollTo({ top: 0, behavior: 'smooth' });
});
});
// Initial fetch
refreshAll();
function refreshAll() {
fetchStats();
fetchRecentLogs();
fetchWater();
fetchWeight();
fetchSupplements();
}
// --- 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);
// Update Consumed (UI)
setText('cal-consumed', consumed.calories);
setText('pro-consumed', consumed.protein);
// Update Left (UI)
setText('cal-left', Math.max(0, remaining.calories));
setText('pro-left', Math.max(0, remaining.protein));
// Update Progress (UI)
updateProgress('cal-progress', consumed.calories, goals.calories);
updateProgress('pro-progress', consumed.protein, goals.protein);
// Creatine status in Health tab
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 (creBadge) {
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 + '%';
}
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;
fetch('api/logs.php?action=get_stats')
.then(res => res.json())
.then(stats => {
const goal = stats.goals.water || 2.5;
// Health Tab
setText('water-consumed', amount.toFixed(2));
setText('water-goal', goal);
updateProgress('water-progress', amount, goal);
// Today Tab
document.querySelectorAll('.today-water-consumed').forEach(el => el.innerText = amount.toFixed(2));
document.querySelectorAll('.today-water-goal').forEach(el => el.innerText = goal);
document.querySelectorAll('.today-water-progress-bar').forEach(el => {
const pct = goal > 0 ? Math.min(100, (amount / goal) * 100) : 0;
el.style.width = pct + '%';
});
});
});
}
document.querySelectorAll('.water-btn').forEach(btn => {
btn.addEventListener('click', () => {
const amount = parseFloat(btn.dataset.amount);
logWater(amount);
});
});
const btnAddWater = document.getElementById('btnAddWater');
if (btnAddWater) {
btnAddWater.addEventListener('click', () => {
logWater(0.25);
});
}
function logWater(amount) {
fetch('api/water.php', {
method: 'POST',
body: JSON.stringify({ amount: amount })
}).then(() => fetchWater());
}
// --- SUPPLEMENTS ---
function fetchSupplements() {
fetch('api/supplements.php?action=get_status')
.then(res => res.json())
.then(data => {
const container = document.getElementById('today-supplements-list');
if (!container) return;
container.innerHTML = data.map(sup => `
<button class="btn btn-sm ${sup.taken ? 'btn-success' : 'btn-outline-secondary'} py-2 px-3 rounded-pill supplement-toggle-btn"
data-id="${sup.id}" data-name="${sup.name}" ${sup.taken ? 'disabled' : ''}>
${sup.name} ${sup.taken ? '✓' : ''}
<div class="x-small text-opacity-50">${sup.default_amount || ''}</div>
</button>
`).join('');
document.querySelectorAll('.supplement-toggle-btn').forEach(btn => {
btn.addEventListener('click', () => {
logSupplement(btn.dataset.id, btn.dataset.name);
});
});
});
}
function logSupplement(id, name) {
fetch('api/supplements.php?action=log', {
method: 'POST',
body: JSON.stringify({ id: id, name: name })
}).then(() => {
fetchSupplements();
fetchStats(); // Update reminders too
});
}
// --- 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);
const weightInput = document.getElementById('weightInput');
if (weightInput) weightInput.value = last.weight;
const photoWeight = document.getElementById('photoWeight');
if (photoWeight) photoWeight.value = last.weight;
}
});
}
if (weightForm) {
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();
}
});
});
}
// --- PHOTO GALLERY ---
function fetchPhotos() {
fetch('api/photos.php')
.then(res => res.json())
.then(data => {
const container = document.getElementById('gallery-container');
if (!container) return;
if (data.length === 0) {
container.innerHTML = '<div class="col-12 text-center py-5"><p class="text-muted small">No photos uploaded yet.</p></div>';
return;
}
container.innerHTML = data.map(photo => `
<div class="col-6">
<div class="card bg-dark border-secondary overflow-hidden h-100">
<img src="${photo.photo_path}" class="card-img-top" style="height: 160px; object-fit: cover;" alt="Progress">
<div class="card-body p-2">
<div class="d-flex justify-content-between align-items-center">
<span class="x-small fw-bold">${photo.logged_at}</span>
${photo.weight ? `<span class="badge bg-secondary x-small">${photo.weight} kg</span>` : ''}
</div>
</div>
</div>
</div>
`).join('');
});
}
if (photoForm) {
photoForm.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData();
formData.append('photo', document.getElementById('photoInput').files[0]);
formData.append('weight', document.getElementById('photoWeight').value);
fetch('api/photos.php', {
method: 'POST',
body: formData
}).then(res => res.json())
.then(res => {
if (res.success) {
bootstrap.Modal.getInstance(document.getElementById('photoModal')).hide();
photoForm.reset();
fetchPhotos();
}
});
});
}
// --- REMINDERS ---
function updateReminders(consumed, goals) {
const row = document.getElementById('reminders-row');
if (!row) return;
row.innerHTML = '';
const items = [
{ label: 'Water', current: 0, goal: goals.water || 2.5, unit: 'L', color: 'info' },
{ label: 'Protein', current: consumed.protein, goal: goals.protein, unit: 'g', color: 'primary' }
];
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 ---
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();
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 } },
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();
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
};
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();
}
});
});
});