// ═══════════════════════════════════════
// GAINZ Web — App Logic
// ═══════════════════════════════════════
var currentPage = 'timeline';
var nutriDate = new Date().toISOString().split('T')[0];
var tmbSex = 'M';
var RATINGS = { 1: '😩 Péssimo', 2: '😔 Ruim', 3: '😐 Bom', 4: '😄 Muito bom', 5: '🔥 Perfeito' };
var MFIELDS = [
{ k: 'weight_kg', l: 'Peso (kg)' }, { k: 'body_fat_pct', l: 'Gordura (%)' },
{ k: 'chest_cm', l: 'Peito (cm)' }, { k: 'waist_cm', l: 'Cintura (cm)' },
{ k: 'hip_cm', l: 'Quadril (cm)' }, { k: 'bicep_cm', l: 'Bíceps (cm)' },
{ k: 'thigh_cm', l: 'Coxa (cm)' }, { k: 'calf_cm', l: 'Panturrilha (cm)' },
{ k: 'neck_cm', l: 'Pescoço (cm)' }
];
var INFO_FIELDS = [
{ k: 'cpf', l: 'CPF' }, { k: 'phone', l: 'Telefone' }, { k: 'birth_date', l: 'Data de Nascimento' },
{ k: 'street', l: 'Rua' }, { k: 'number', l: 'Número' }, { k: 'complement', l: 'Complemento' },
{ k: 'neighborhood', l: 'Bairro' }, { k: 'city', l: 'Cidade' }, { k: 'state', l: 'Estado (UF)' }, { k: 'zip_code', l: 'CEP' }
];
// ─── Navigation ──────────────────────
function navigateTo(page) {
currentPage = page;
document.querySelectorAll('.page').forEach(function (p) { p.classList.remove('active'); });
var el = document.getElementById('page-' + page);
if (el) el.classList.add('active');
document.querySelectorAll('.nav-item').forEach(function (n) { n.classList.toggle('active', n.dataset && n.dataset.page === page); });
var titles = { timeline: 'GAINZ', treinos: 'Musculação', arlivre: 'Ar livre', nutricional: 'Reg. Nutricional', historico: 'Histórico', medicoes: 'Medições', conquistas: 'Conquistas', calculadoras: 'Calculadoras', perfil: 'Perfil', info: 'Informações', plans: 'Planos', nutriHistory: 'Hist. Nutricional', groupDetail: 'Grupo', templates: 'Treinos Prontos', apoiar: 'Apoiar' };
document.getElementById('pageTitle').textContent = titles[page] || 'GAINZ';
closeSidebar();
loadPage(page);
}
function openSidebar() { document.getElementById('sidebar').classList.add('open'); document.getElementById('sidebarOverlay').classList.add('open'); }
function closeSidebar() { document.getElementById('sidebar').classList.remove('open'); document.getElementById('sidebarOverlay').classList.remove('open'); }
function showLogin() { document.getElementById('loginScreen').style.display = ''; document.getElementById('appScreen').style.display = 'none'; }
function showApp() { document.getElementById('loginScreen').style.display = 'none'; document.getElementById('appScreen').style.display = ''; updateSidebar(); navigateTo('timeline'); if (typeof startNotifPolling === 'function') startNotifPolling(); }
function updateSidebar() {
if (!USER) return;
var i = (USER.display_name || USER.username || '?').split(' ').map(function (w) { return w[0]; }).join('').substring(0, 2).toUpperCase();
document.getElementById('sidebarAvatar').innerHTML = USER.avatar_url ? ' ' : i;
document.getElementById('sidebarName').textContent = USER.display_name || USER.username;
document.getElementById('sidebarUsername').textContent = '@' + USER.username;
if (USER.page_access === 1) { document.getElementById('adminSep').style.display = ''; document.getElementById('adminNav').style.display = ''; document.getElementById('createPostNav').style.display = ''; }
}
function loadPage(p) {
var loaders = { timeline: loadTimeline, treinos: loadTreinos, arlivre: loadArLivre, nutricional: loadNutri, historico: loadHistory, medicoes: loadMeasurements, conquistas: loadConquistas, perfil: loadProfile, info: loadInfo, plans: loadPlans, nutriHistory: loadNutriHistory, calculadoras: function () { }, apoiar: loadApoiar, templates: loadTemplates };
if (loaders[p]) loaders[p]();
}
function openModal(id) { document.getElementById(id).classList.add('active'); }
function closeModal(id) { document.getElementById(id).classList.remove('active'); }
// Timeline + social features movidos para social.js
function getYtId(url) { var m = url.match(/(?:v=|youtu\.be\/|embed\/|shorts\/)([a-zA-Z0-9_-]{11})/); return m ? m[1] : ''; }
// Create post
function openCreatePostModal() { Q('#postContent').value = ''; Q('#postMediaType').value = 'none'; Q('#postMediaUrl').value = ''; openModal('createPostModal'); }
async function savePost() {
var c = Q('#postContent').value.trim(); if (!c) return toast('Conteúdo obrigatório', true);
var b = { content: c, media_type: Q('#postMediaType').value }; if (b.media_type !== 'none') b.media_url = Q('#postMediaUrl').value.trim();
try { await api('posts', 'POST', b); closeModal('createPostModal'); loadTimeline(); toast('Publicado!'); } catch (e) { toast(e.message, true); }
}
// ═══ TREINOS ════════════════════════
var currentGroupId = null;
async function loadTreinos() {
try {
var [groups, ungrouped] = await Promise.all([api('groups'), api('routines&group_id=0')]);
var h = '';
groups.forEach(function (g) {
h += '
' + esc(g.name) + ' ' + g.routine_count + ' rotinas
› ×
';
});
Q('#groupsList').innerHTML = h;
var rh = '';
if (ungrouped.length && groups.length) rh += 'Sem grupo
';
ungrouped.forEach(function (r) {
rh += '' + esc(r.name) + ' ';
if (r.description) rh += '
' + esc(r.description) + '
';
rh += '
' + r.exercise_count + ' ex ';
if (groups.length) rh += 'Mover ';
rh += '
';
});
Q('#routinesList').innerHTML = rh || (!groups.length ? '' : '');
} catch (e) { }
}
function openGroup(id, name) {
currentGroupId = id;
document.getElementById('pageTitle').textContent = name;
document.querySelectorAll('.page').forEach(function (p) { p.classList.remove('active'); });
Q('#page-groupDetail').classList.add('active');
loadGroupRoutines(id);
}
async function loadGroupRoutines(gid) {
try {
var data = await api('routines&group_id=' + gid);
var h = '‹ Voltar ';
h += '+ Nova rotina neste grupo ';
if (!data.length) h += 'Nenhuma rotina neste grupo
';
data.forEach(function (r) {
h += '' + esc(r.name) + ' ';
if (r.description) h += '
' + esc(r.description) + '
';
h += '
' + r.exercise_count + ' exercícios ';
});
Q('#groupDetailContent').innerHTML = h;
} catch (e) { }
}
async function viewRoutine(id) {
try {
var r = await api('routines/' + id);
document.querySelectorAll('.page').forEach(function (p) { p.classList.remove('active'); });
Q('#page-routineDetail').classList.add('active');
document.getElementById('pageTitle').textContent = r.name;
var h = '‹ Voltar ';
h += '' + esc(r.name) + ' ';
if (r.description) h += '' + esc(r.description) + '
';
if (r.exercises && r.exercises.length) {
r.exercises.forEach(function (ex, i) {
h += '';
if (ex.notes) h += '
' + esc(ex.notes) + '
';
h += '
';
});
} else { h += ''; }
h += 'Iniciar Treino Voltar Remover
';
Q('#routineDetailContent').innerHTML = h;
} catch (e) { toast(e.message, true); }
}
// ═══ ACTIVE WORKOUT ═════════════════
var activeSession = null;
async function startWorkout(routineId, name) {
try {
var data = await api('sessions', 'POST', { routine_id: routineId });
activeSession = { id: data.session_id, name: name, exercises: data.exercises || [], startedAt: Date.now() };
// Montar sets pra cada exercício
activeSession.exercises = activeSession.exercises.map(function (ex, i) {
return { ...ex, sets: Array.from({ length: parseInt(ex.sets_target) || 3 }, function (_, s) { return { key: i + '_' + s, weight: '', reps: '', done: false, serverId: null }; }) };
});
showActiveWorkout();
} catch (e) { toast(e.message, true); }
}
function showActiveWorkout() {
document.querySelectorAll('.page').forEach(function (p) { p.classList.remove('active'); });
Q('#page-activeWorkout').classList.add('active');
document.getElementById('pageTitle').textContent = activeSession.name;
renderActiveWorkout();
}
function renderActiveWorkout() {
if (!activeSession) return;
var elapsed = Math.floor((Date.now() - activeSession.startedAt) / 1000);
var m = Math.floor(elapsed / 60).toString().padStart(2, '0'), s = (elapsed % 60).toString().padStart(2, '0');
var h = '
' + esc(activeSession.name) + ' ' + m + ':' + s + ' ';
h += 'Cancelar Finalizar
';
activeSession.exercises.forEach(function (ex, ei) {
h += '';
});
Q('#activeWorkoutContent').innerHTML = h;
}
async function checkWebSet(ei, si) {
var set = activeSession.exercises[ei].sets[si];
var wInput = Q('#w_' + ei + '_' + si), rInput = Q('#r_' + ei + '_' + si);
if (set.done) {
// Uncheck
if (set.serverId) try { await api('sets/' + set.serverId, 'DELETE', {}); } catch (e) { }
set.done = false; set.serverId = null;
} else {
// Check
set.weight = wInput.value; set.reps = rInput.value;
var w = parseFloat(set.weight) || 0, r = parseInt(set.reps) || 0;
try {
var res = await api('sets', 'POST', { session_id: activeSession.id, exercise_name: activeSession.exercises[ei].name, weight_kg: w, reps: r });
set.serverId = res.id || null;
} catch (e) { }
set.done = true;
}
renderActiveWorkout();
}
function addWebSet(ei) {
var ex = activeSession.exercises[ei];
ex.sets.push({ key: ei + '_' + ex.sets.length, weight: '', reps: '', done: false, serverId: null });
renderActiveWorkout();
}
async function finishWorkout() {
if (!confirm('Finalizar treino?')) return;
try { await api('sessions/' + activeSession.id + '/finish', 'POST', {}); } catch (e) { }
var sid = activeSession.id;
activeSession = null;
toast('Treino finalizado!');
navigateTo('historico');
}
async function cancelWorkout() {
if (!confirm('Cancelar treino? Dados serão perdidos.')) return;
try { await api('sessions/' + activeSession.id, 'DELETE', {}); } catch (e) { }
activeSession = null;
navigateTo('treinos');
}
// Timer update
setInterval(function () {
if (!activeSession) return;
var el = Q('#workoutTimer'); if (!el) return;
var elapsed = Math.floor((Date.now() - activeSession.startedAt) / 1000);
el.textContent = Math.floor(elapsed / 60).toString().padStart(2, '0') + ':' + (elapsed % 60).toString().padStart(2, '0');
}, 1000);
async function deleteRoutine(id) { if (!confirm('Remover rotina?')) return; try { await api('routines/' + id, 'DELETE', {}); navigateTo('treinos'); toast('Removida'); } catch (e) { } }
async function deleteGroup(id) { if (!confirm('Remover grupo?')) return; try { await api('groups/' + id, 'DELETE', {}); loadTreinos(); } catch (e) { } }
async function moveRoutine(id) {
try {
var gs = await api('groups'); if (!gs.length) return toast('Crie um grupo primeiro', true);
var choice = prompt('Mover para qual grupo?\n\n' + gs.map(function (g) { return g.name; }).join(', '));
if (!choice) return; var g = gs.find(function (g) { return g.name.toLowerCase() === choice.toLowerCase(); });
if (!g) return toast('Grupo não encontrado', true);
await api('routines/' + id, 'PUT', { group_id: g.id }); loadTreinos(); toast('Movido!');
} catch (e) { }
}
// Group modal
function openNewGroupModal() { Q('#groupName').value = ''; openModal('groupModal'); }
var _savingGroup = false;
async function saveGroup() {
if (_savingGroup) return;
var n = Q('#groupName').value.trim();
if (!n) return toast('Nome obrigatório', true);
_savingGroup = true;
try { await api('groups', 'POST', { name: n }); closeModal('groupModal'); loadTreinos(); toast('Grupo criado!'); }
catch (e) { toast(e.message, true); }
_savingGroup = false;
}
// Routine modal with exercises
var routineExercises = [];
async function openNewRoutineModal(gid) {
Q('#routineName').value = ''; Q('#routineDesc').value = '';
routineExercises = [{ name: '', sets_target: 3, reps_target: '10-12', rest_seconds: 60 }];
Q('#routineGroupId').value = gid || '';
try { var gs = await api('groups'); var sel = Q('#routineGroupSel'); sel.innerHTML = 'Sem grupo '; gs.forEach(function (g) { sel.innerHTML += '' + esc(g.name) + ' '; }); } catch (e) { }
renderRoutineExercises();
openModal('routineModal');
}
function renderRoutineExercises() {
var h = '';
routineExercises.forEach(function (ex, i) {
h += 'Exercício ' + (i + 1) + ' ×
';
h += '
';
h += '
';
});
h += '+ Adicionar Exercício
';
Q('#routineExList').innerHTML = h;
}
function addRoutineEx() { routineExercises.push({ name: '', sets_target: 3, reps_target: '10-12', rest_seconds: 60 }); renderRoutineExercises(); }
function removeRoutineEx(i) { routineExercises.splice(i, 1); if (!routineExercises.length) routineExercises.push({ name: '', sets_target: 3, reps_target: '10-12', rest_seconds: 60 }); renderRoutineExercises(); }
var _savingRoutine = false;
async function saveRoutine() {
if (_savingRoutine) return;
var n = Q('#routineName').value.trim(); if (!n) return toast('Nome obrigatório', true);
var gid = Q('#routineGroupSel').value || Q('#routineGroupId').value || null;
var exs = routineExercises.filter(function (e) { return e.name.trim(); });
_savingRoutine = true;
try { await api('routines', 'POST', { name: n, description: Q('#routineDesc').value.trim(), group_id: gid, exercises: exs }); closeModal('routineModal'); loadTreinos(); toast('Rotina criada!'); }
catch (e) { toast(e.message, true); }
_savingRoutine = false;
}
// ═══ NUTRICIONAL ════════════════════
// ═══ TEMPLATES (Treinos Prontos) ══════════════
async function loadTemplates() {
try {
var ts = await api('templates');
if (!ts || !ts.length) { Q('#templatesList').innerHTML = 'Nenhum treino pronto disponível
'; return; }
var h = '';
ts.forEach(function(t) {
var diff = t.difficulty ? '' + esc(t.difficulty) + ' ' : '';
h += ''
+ '
' + esc(t.name) + ' ' + diff + '
'
+ (t.description ? '
' + esc(t.description) + '
' : '')
+ '
' + (t.routine_count || 0) + ' rotinas · ' + esc(t.target_audience || 'Geral') + '
'
+ '
› ';
});
Q('#templatesList').innerHTML = h;
} catch (e) { Q('#templatesList').innerHTML = ''; }
}
async function viewTemplate(id) {
try {
var t = await api('templates/' + id);
document.querySelectorAll('.page').forEach(function(p) { p.classList.remove('active'); });
Q('#page-templateDetail').classList.add('active');
document.getElementById('pageTitle').textContent = t.name || 'Template';
var h = '‹ Voltar ';
h += '' + esc(t.name) + ' ';
if (t.description) h += '' + esc(t.description) + '
';
(t.routines || []).forEach(function(r) {
h += '' + esc(r.name) + ' ';
if (r.description) h += '
' + esc(r.description) + '
';
h += '
';
(r.exercises || []).forEach(function(ex) {
h += '
• ' + esc(ex.name) + ' · ' + (ex.sets_target || 3) + 'x' + esc(ex.reps_target || '10') + '
';
});
h += '
';
});
h += 'Usar este treino ';
Q('#templateDetailContent').innerHTML = h;
} catch (e) { toast('Erro ao carregar template', true); }
}
async function importTemplate(id) {
if (!confirm('Importar este treino? Vai criar grupo + rotinas + exercícios no seu perfil.')) return;
try {
await api('templates/' + id + '/import', 'POST', {});
toast('Treino importado! Abrindo Musculação...');
navigateTo('treinos');
} catch (e) { toast(e.message || 'Falha ao importar', true); }
}
function changeNutriDate(d) { var dt = new Date(nutriDate); dt.setDate(dt.getDate() + d); nutriDate = dt.toISOString().split('T')[0]; loadNutri(); }
function nowTime() { var n = new Date(); return String(n.getHours()).padStart(2, '0') + ':' + String(n.getMinutes()).padStart(2, '0'); }
async function loadNutri() {
var now = new Date(), today = now.toISOString().split('T')[0], yd = new Date(now); yd.setDate(yd.getDate() - 1); var yesterday = yd.toISOString().split('T')[0];
Q('#nutriDateLabel').textContent = nutriDate === today ? 'Hoje' : nutriDate === yesterday ? 'Ontem' : fmtDate(nutriDate);
try {
var [meals, pd, water] = await Promise.all([api('meals&date=' + nutriDate), api('plan-for-date&date=' + nutriDate), api('water&date=' + nutriDate)]);
meals = Array.isArray(meals) ? meals : [];
var plan = pd && pd.plan, tc = 0, tp = 0, tca = 0, tf = 0;
meals.forEach(function (m) { tc += parseInt(m.calories || 0); tp += parseFloat(m.protein || 0); tca += parseFloat(m.carbs || 0); tf += parseFloat(m.fat || 0); });
var tgt = plan ? parseInt(plan.target_calories) : 0, rem = tgt - tc, over = tc > tgt && tgt > 0, pct = tgt ? Math.min(tc / tgt, 1) : 0;
// Ring
Q('#nutriRing').innerHTML = svgRing(140, 62, 8, pct, over ? 'var(--red)' : 'var(--accent)') + '' + tc + '
' + (tgt ? '
/ ' + tgt + '
' + (over ? '+' + Math.abs(rem) + ' acima' : rem + ' restante') + '
' : '
consumido
') + '
';
// Pills
var pp = plan ? parseInt(plan.target_protein) : 0, pc = plan ? parseInt(plan.target_carbs) : 0, pf = plan ? parseInt(plan.target_fat) : 0;
Q('#nutriPills').innerHTML = mkPill('Proteína', tp, pp, 'var(--green)') + mkPill('Carbs', tca, pc, 'var(--warn)') + mkPill('Gordura', tf, pf, 'var(--purple)');
// Water
var wt = water ? water.total || 0 : 0, wg = water ? water.target || 2000 : 2000, wp = Math.min(wt / wg, 1);
Q('#waterSection').innerHTML = ''
+ '
'
+ svgRing(86, 36, 6, wp, 'var(--water)')
+ '
'
+ '
' + Math.round(wp) + '%
'
+ '
' + wt + '/' + wg + '
'
+ '
'
+ '
'
+ '
💧 Água '
+ '
' + wt + 'ml / ' + wg + 'ml
'
+ '
' + [200, 300, 500].map(function (ml) { return '' + ml + 'ml '; }).join('') + '+ / Meta
';
// Meals
if (!meals.length) { Q('#mealsList').innerHTML = ''; }
else {
var mh = '';
meals.forEach(function (m) {
mh += '' + esc(m.title) + ' ' + (m.meal_time ? m.meal_time.substring(0, 5) : '--:--') + '
' + m.calories + ' kcal
P' + m.protein + 'g · C' + m.carbs + 'g · G' + m.fat + 'g
';
mh += '
Editar Remover
';
});
Q('#mealsList').innerHTML = mh;
}
// Total
Q('#nutriTotal').style.display = ''; Q('#nutriTotal').innerHTML = 'Meta
' + (tgt || '--') + '
';
} catch (e) { }
}
function mkPill(l, c, t, co) { var p = t ? Math.min(c / t * 100, 100) : 0; return '' + Math.round(c) + (t ? '/' + t : '') + 'g
' + l + '
'; }
async function addWater(ml) { try { await api('water', 'POST', { amount_ml: ml, date: nutriDate }); loadNutri(); } catch (e) { } }
// Water modal
async function openWaterModal() {
try { var w = await api('water&date=' + nutriDate); Q('#waterTargetInput').value = w.target || 2000; Q('#customWaterInput').value = ''; } catch (e) { }
// Load logs
try {
var w = await api('water&date=' + nutriDate);
var lh = ''; (w.logs || []).forEach(function (l) {
lh += '' + l.amount_ml + 'ml ' + (l.created_at ? new Date(l.created_at).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) : '') + ' ×
';
});
Q('#waterLogsList').innerHTML = lh || 'Sem registros
';
} catch (e) { }
openModal('waterModal');
}
async function addCustomWater() { var ml = parseInt(Q('#customWaterInput').value); if (!ml || ml <= 0) return; try { await api('water', 'POST', { amount_ml: ml, date: nutriDate }); Q('#customWaterInput').value = ''; openWaterModal(); loadNutri(); } catch (e) { } }
async function saveWaterTarget() { var t = parseInt(Q('#waterTargetInput').value); if (!t || t < 500 || t > 10000) return toast('Meta entre 500 e 10000ml', true); try { await api('water/target', 'POST', { target: t }); closeModal('waterModal'); loadNutri(); toast('Meta atualizada!'); } catch (e) { toast(e.message, true); } }
async function deleteWater(id) { try { await api('water/' + id, 'DELETE', {}); openWaterModal(); loadNutri(); } catch (e) { } }
// Meal modal
function openMealModal() { Q('#mealModalTitle').textContent = 'Nova Refeição'; Q('#mealEditId').value = ''; ['mealTitle', 'mealCal', 'mealTime', 'mealProt', 'mealCarbs', 'mealFat'].forEach(function (id) { Q('#' + id).value = ''; }); Q('#mealTime').value = nowTime(); openModal('mealModal'); }
function editMeal(id, t, c, tm, p, ca, f) { Q('#mealModalTitle').textContent = 'Editar Refeição'; Q('#mealEditId').value = id; Q('#mealTitle').value = t; Q('#mealCal').value = c; Q('#mealTime').value = tm; Q('#mealProt').value = p; Q('#mealCarbs').value = ca; Q('#mealFat').value = f; openModal('mealModal'); }
var _savingMeal = false;
async function saveMeal() {
if (_savingMeal) return;
var t = Q('#mealTitle').value.trim();
if (!t) return toast('Título obrigatório', true);
var b = { title: t, calories: parseInt(Q('#mealCal').value) || 0, protein: parseFloat(Q('#mealProt').value) || 0, carbs: parseFloat(Q('#mealCarbs').value) || 0, fat: parseFloat(Q('#mealFat').value) || 0, meal_time: Q('#mealTime').value || null, date: nutriDate };
var id = Q('#mealEditId').value;
_savingMeal = true;
try { if (id) await api('meals/' + id, 'PUT', b); else await api('meals', 'POST', b); closeModal('mealModal'); loadNutri(); toast('Salvo!'); }
catch (e) { toast(e.message, true); }
_savingMeal = false;
}
async function deleteMeal(id) { if (!confirm('Remover refeição?')) return; try { await api('meals/' + id, 'DELETE', {}); loadNutri(); } catch (e) { } }
// ═══ HISTORY ════════════════════════
function cleanNote(n) { if (!n) return null; var c = n; ['Péssimo', 'Ruim', 'Bom', 'Muito bom', 'Perfeito'].forEach(function (l) { c = c.replace(new RegExp('^.*' + l + '\\n?'), ''); }); c = c.replace(/^[😩😔😐😄🔥]\s*/g, '').trim(); return c || null; }
async function loadHistory() {
try {
var [ss, st] = await Promise.all([api('sessions'), api('streak')]);
Q('#streakStats').innerHTML = '' + (st.streak || 0) + '
Dias seguidos
' + (st.total_workouts || 0) + '
Treinos total
';
if (!ss.length) { Q('#historyList').innerHTML = ''; return; }
var h = ''; ss.forEach(function (s) {
var dt = new Date(s.started_at), dur = s.finished_at ? Math.round((new Date(s.finished_at) - dt) / 60000) : null, vol = s.total_volume ? Math.round(parseFloat(s.total_volume)) : null;
h += '' + esc(s.name) + ' ' + dt.toLocaleDateString('pt-BR', { day: '2-digit', month: 'short' }) + '
Séries: ' + s.set_count + ' ';
if (dur) h += 'Duração: ' + dur + 'min '; if (vol) h += 'Volume: ' + vol + 'kg ';
if (s.rating) h += '' + (RATINGS[s.rating] || '') + ' '; h += '
';
var cn = cleanNote(s.notes); if (cn) h += '
Nota: ' + esc(cn) + '
';
h += '
×
';
}); Q('#historyList').innerHTML = h;
} catch (e) { }
}
async function deleteSession(id, n) { if (!confirm('Remover "' + n + '"?')) return; try { await api('sessions/' + id, 'DELETE', {}); loadHistory(); toast('Removido'); } catch (e) { } }
// ═══ MEASUREMENTS ═══════════════════
var _measChartField = 'weight_kg';
function renderMeasChart(records, field) {
// records é do mais recente pro mais antigo — inverto
var points = records.slice().reverse().filter(function(r) { return r[field] != null; });
if (points.length < 2) return 'Precisa pelo menos 2 registros com "' + (MFIELDS.find(f => f.k === field) || {l: field}).l + '" pra mostrar gráfico
';
var W = 400, H = 140, PAD = 30;
var vals = points.map(function(p) { return parseFloat(p[field]); });
var min = Math.min.apply(null, vals), max = Math.max.apply(null, vals);
if (min === max) { min -= 1; max += 1; }
var pad = (max - min) * 0.1; min -= pad; max += pad;
var xStep = (W - PAD * 2) / (points.length - 1);
var yScale = function(v) { return H - PAD - ((v - min) / (max - min)) * (H - PAD * 2); };
var pathD = '';
var dots = '';
points.forEach(function(p, i) {
var x = PAD + i * xStep, y = yScale(parseFloat(p[field]));
pathD += (i === 0 ? 'M' : 'L') + x.toFixed(1) + ',' + y.toFixed(1);
dots += ' ';
});
// Área sob a curva
var areaD = pathD + ' L' + (PAD + (points.length - 1) * xStep).toFixed(1) + ',' + (H - PAD) + ' L' + PAD + ',' + (H - PAD) + ' Z';
// Labels nos extremos
var first = points[0][field], last = points[points.length - 1][field];
var diff = (parseFloat(last) - parseFloat(first)).toFixed(1);
var diffColor = diff > 0 ? 'var(--green)' : diff < 0 ? 'var(--red)' : 'var(--text3)';
var diffPrefix = diff > 0 ? '+' : '';
var svg = ''
+ ' '
+ ' '
+ ' '
+ dots
+ '' + parseFloat(first).toFixed(1) + ' '
+ '' + parseFloat(last).toFixed(1) + ' '
+ ' ';
return ''
+ '
'
+ '
' + (MFIELDS.find(f => f.k === field) || {l: field}).l + ' ' + diffPrefix + diff + '
'
+ '
' + points.length + ' registros
'
+ '
'
+ svg
+ '
';
}
function renderMeasFieldPicker(records) {
// Mostra só campos que têm ao menos 2 registros
var avail = MFIELDS.filter(function(f) { return records.filter(function(r) { return r[f.k] != null; }).length >= 1; });
if (!avail.length) return '';
var h = '';
avail.forEach(function(f) {
var active = f.k === _measChartField;
h += '' + f.l.split(' ')[0] + ' ';
});
h += '
';
return h;
}
function setMeasField(k) { _measChartField = k; loadMeasurements(); }
async function loadMeasurements() {
try {
var data = await api('measurements');
if (!data.length) { Q('#measurementsList').innerHTML = ''; return; }
var h = '';
// Gráfico no topo
h += renderMeasFieldPicker(data);
h += renderMeasChart(data, _measChartField);
// Lista de registros
h += 'Histórico ';
data.forEach(function (r) {
h += '' + fmtDate(r.date) + ' Remover
';
MFIELDS.forEach(function (f) { if (r[f.k]) h += '
' + f.l.split(' ')[0] + '
' + r[f.k] + '
'; });
h += '
';
});
Q('#measurementsList').innerHTML = h;
} catch (e) { Q('#measurementsList').innerHTML = ''; }
}
function openMeasurementModal() {
var h = ''; MFIELDS.forEach(function (f) { h += '' + f.l + '
'; });
Q('#measureFields').innerHTML = h; openModal('measureModal');
}
async function saveMeasurement() { var b = { date: new Date().toISOString().split('T')[0] }; MFIELDS.forEach(function (f) { var v = Q('#m_' + f.k).value; if (v) b[f.k] = parseFloat(v); }); try { await api('measurements', 'POST', b); closeModal('measureModal'); loadMeasurements(); toast('Salvo!'); } catch (e) { toast(e.message, true); } }
async function deleteMeasurement(id) { if (!confirm('Remover medição?')) return; try { await api('measurements/' + id, 'DELETE', {}); loadMeasurements(); } catch (e) { } }
// ═══ NUTRITION HISTORY ══════════════
var nutriHistDays = 14;
async function loadNutriHistory() {
try {
var d = await api('nutrition-history&days=' + nutriHistDays);
var days = d.days || [], wt = d.water_target || 2000;
var avgCal = days.length ? Math.round(days.reduce(function (s, d) { return s + d.calories; }, 0) / days.length) : 0;
var avgW = days.length ? Math.round(days.reduce(function (s, d) { return s + d.water_ml; }, 0) / days.length) : 0;
var metW = days.filter(function (d) { return d.water_ml >= wt; }).length;
var h = '‹ Voltar ';
h += '' + [7, 14, 30].map(function (p) { return '
' + p + 'd
'; }).join('') + '
';
h += '' + avgCal + '
kcal/dia média
' + metW + '/' + days.length + '
dias meta água
';
h += 'Detalhado
';
days.slice().reverse().filter(function (d) { return d.calories > 0 || d.water_ml > 0; }).forEach(function (d) {
var dt = new Date(d.date + 'T12:00:00'), met = d.water_ml >= wt;
h += '' + dt.toLocaleDateString('pt-BR', { day: '2-digit', month: 'short', weekday: 'short' }) + ' ' + d.calories + 'kcal P' + Math.round(d.protein) + 'g ' + d.water_ml + 'ml' + (met ? ' ✓' : '') + '
';
});
Q('#nutriHistoryContent').innerHTML = h;
} catch (e) { Q('#nutriHistoryContent').innerHTML = ''; }
}
// ═══ PLANS ══════════════════════════
var _plansCache = [];
var _wdCache = {};
async function loadPlans() {
try {
var [plans, wd] = await Promise.all([api('plans'), api('weekday-plans')]);
_plansCache = plans;
_wdCache = wd;
var h = '‹ Voltar ';
h += '';
plans.forEach(function (p) {
h += ''
+ '
'
+ '' + esc(p.name) + ' '
+ '' + p.target_calories + 'kcal
'
+ '
P' + p.target_protein + 'g · C' + p.target_carbs + 'g · G' + p.target_fat + 'g
'
+ '
'
+ 'Editar '
+ 'Apagar '
+ '
';
});
if (!plans.length) h += 'Sem planos
Crie um plano pra começar
';
var DAYS = ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'];
h += 'Dias da semana ';
h += 'Clique num dia pra atribuir um plano
';
h += '';
DAYS.forEach(function (d, i) {
var a = wd[i];
h += '
'
+ '
' + d + '
'
+ '
' + (a ? esc(a.name) : '--') + '
'
+ '
';
});
h += '
';
Q('#plansContent').innerHTML = h;
} catch (e) { Q('#plansContent').innerHTML = ''; }
}
function openPlanModal() {
Q('#planModalTitle').textContent = 'Novo Plano';
Q('#planEditId').value = '';
Q('#planSaveBtn').textContent = 'Criar';
['planName', 'planCal', 'planProt', 'planCarbs', 'planFat'].forEach(function (id, i) { Q('#' + id).value = ['', '1900', '150', '190', '55'][i]; });
openModal('planModal');
}
function openEditPlan(id) {
var p = _plansCache.find(function(x) { return x.id == id; });
if (!p) return;
Q('#planModalTitle').textContent = 'Editar Plano';
Q('#planEditId').value = id;
Q('#planSaveBtn').textContent = 'Salvar';
Q('#planName').value = p.name;
Q('#planCal').value = p.target_calories;
Q('#planProt').value = p.target_protein;
Q('#planCarbs').value = p.target_carbs;
Q('#planFat').value = p.target_fat;
openModal('planModal');
}
async function savePlan() {
var n = Q('#planName').value.trim();
if (!n) return toast('Nome obrigatório', true);
var body = {
name: n,
target_calories: parseInt(Q('#planCal').value) || 1900,
target_protein: parseInt(Q('#planProt').value) || 150,
target_carbs: parseInt(Q('#planCarbs').value) || 190,
target_fat: parseInt(Q('#planFat').value) || 55,
};
var id = Q('#planEditId').value;
try {
if (id) { await api('plans/' + id, 'PUT', body); toast('Plano atualizado!'); }
else { await api('plans', 'POST', body); toast('Plano criado!'); }
closeModal('planModal');
loadPlans();
} catch (e) { toast(e.message, true); }
}
async function deletePlan(id) {
if (!confirm('Apagar este plano?')) return;
try { await api('plans/' + id, 'DELETE', {}); loadPlans(); toast('Removido'); }
catch (e) { toast(e.message || 'Falha', true); }
}
// Assign plan to a specific weekday
var _assigningWeekday = null;
function openAssignWeekday(weekdayIndex) {
_assigningWeekday = weekdayIndex;
var DAYS_FULL = ['Domingo', 'Segunda', 'Terça', 'Quarta', 'Quinta', 'Sexta', 'Sábado'];
Q('#weekdayModalTitle').textContent = 'Plano de ' + DAYS_FULL[weekdayIndex];
var h = '';
var current = _wdCache[weekdayIndex];
h += 'Nenhum Sem plano atribuído
';
_plansCache.forEach(function(p) {
var isCur = current && current.plan_id == p.id;
h += '' + esc(p.name) + ' ' + p.target_calories + 'kcal
P' + p.target_protein + 'g · C' + p.target_carbs + 'g · G' + p.target_fat + 'g
';
});
if (!_plansCache.length) h += '';
Q('#weekdayPlansList').innerHTML = h;
openModal('weekdayModal');
}
async function assignPlan(planId) {
// Builds full assignments map (preserve outros dias)
var assignments = {};
Object.keys(_wdCache).forEach(function(k) {
if (_wdCache[k] && _wdCache[k].plan_id) assignments[k] = _wdCache[k].plan_id;
});
if (planId) assignments[_assigningWeekday] = planId;
else delete assignments[_assigningWeekday];
try {
await api('weekday-plans', 'POST', { assignments: assignments });
closeModal('weekdayModal');
loadPlans();
toast('Atualizado!');
} catch (e) { toast(e.message || 'Falha', true); }
}
// ═══ PROFILE ════════════════════════
async function loadProfile() {
if (!USER) return;
var i = (USER.display_name || USER.username || '?').split(' ').map(function (w) { return w[0]; }).join('').substring(0, 2).toUpperCase();
Q('#profileAvatar').innerHTML = USER.avatar_url ? ' ' : i;
Q('#profileName').textContent = USER.display_name || USER.username;
Q('#profileUsername').textContent = '@' + USER.username;
Q('#editDisplayName').value = USER.display_name || '';
Q('#editBio').value = USER.bio || '';
Q('#editIsPrivate').checked = !!USER.is_private;
// Badge de privacidade no próprio perfil
var privBadge = Q('#profilePrivacyBadge');
if (privBadge) privBadge.style.display = USER.is_private ? 'block' : 'none';
try {
// Social stats via perfil próprio
var me = await api('users/' + USER.id).catch(function() { return null; });
if (me) {
Q('#profileFollowers').textContent = me.followers || 0;
Q('#profileFollowing').textContent = me.following || 0;
Q('#profilePostsCount').textContent = me.post_count || 0;
}
// Badge de solicitações pendentes (só mostra se user é privado)
if (typeof updatePendingRequestsBadge === 'function') updatePendingRequestsBadge();
var [st, rt] = await Promise.all([api('streak'), api('rating-stats')]);
var h = '' + (st.streak || 0) + '
Dias seguidos
' + (st.total_workouts || 0) + '
Treinos total
';
if (rt) h += [1, 2, 3, 4, 5].map(function (r) { return '' + ['😩', '😔', '😐', '😄', '🔥'][r - 1] + '
' + (rt[r] || 0) + '
'; }).join('');
Q('#profileStats').innerHTML = h;
} catch (e) { }
}
async function saveProfile() {
var n = Q('#editDisplayName').value.trim();
var b = Q('#editBio').value.trim();
var priv = Q('#editIsPrivate').checked ? 1 : 0;
if (!n) return;
try {
await api('auth/profile', 'POST', { display_name: n, bio: b, is_private: priv });
USER.display_name = n; USER.bio = b; USER.is_private = priv; updateSidebar(); loadProfile(); toast('Salvo!');
} catch (e) { toast(e.message, true); }
}
async function uploadAvatar(input) {
var file = input.files[0]; if (!file) return;
var reader = new FileReader(); reader.onload = async function () { try { await api('auth/profile', 'POST', { avatar_base64: reader.result }); var d = await api('auth/check'); if (d.success) { USER = d.user; CSRF = d.csrf; updateSidebar(); loadProfile(); } toast('Foto atualizada!'); } catch (e) { toast(e.message, true); } }; reader.readAsDataURL(file);
}
async function removeAvatar() { if (!confirm('Remover foto?')) return; try { await api('auth/profile', 'POST', { avatar_base64: '' }); USER.avatar_url = null; updateSidebar(); loadProfile(); toast('Foto removida'); } catch (e) { } }
function viewPhoto() { if (!USER || !USER.avatar_url) return; Q('#photoModalImg').src = USER.avatar_url; Q('#photoModal').classList.add('active'); }
// ═══ INFO ═══════════════════════════
async function loadInfo() {
try {
var d = await api('auth/info'); var info = d.info || {};
var h = ''; INFO_FIELDS.forEach(function (f) { h += '
' + f.l + '
'; });
h += '
Salvar informações ';
Q('#infoContent').innerHTML = h;
} catch (e) { }
}
async function saveInfo() { var b = {}; INFO_FIELDS.forEach(function (f) { b[f.k] = Q('#info_' + f.k).value.trim() || null; }); try { await api('auth/info', 'POST', b); toast('Salvo!'); } catch (e) { toast(e.message, true); } }
// ═══ CALCULATORS ════════════════════
function calc1RM() {
var w = parseFloat(Q('#rmWeight').value) || 0, r = parseInt(Q('#rmReps').value) || 0;
if (!w || !r) { Q('#rmResult').innerHTML = ''; return; }
var rm = Math.round(w * (1 + r / 30));
var h = '1RM Estimado
' + rm + ' kg
';
[1, 2, 3, 5, 8, 10, 12, 15].forEach(function (rep) { h += '' + rep + ' rep ' + Math.round(rm * (1 - rep / 30)) + ' kg ' + Math.round((1 - rep / 30) * 100) + '%
'; });
Q('#rmResult').innerHTML = h;
}
function calcIMC() {
var w = parseFloat(Q('#imcWeight').value) || 0, h = parseFloat(Q('#imcHeight').value) / 100 || 0;
if (!w || !h) { Q('#imcResult').innerHTML = ''; return; }
var i = (w / (h * h)).toFixed(1), c = i < 18.5 ? 'Abaixo' : i < 25 ? 'Normal' : i < 30 ? 'Sobrepeso' : 'Obesidade';
Q('#imcResult').innerHTML = 'IMC: ' + c + '
' + i + ' kg/m²
';
}
function calcTMB() {
var w = parseFloat(Q('#tmbWeight').value) || 0, h = parseFloat(Q('#tmbHeight').value) || 0, a = parseInt(Q('#tmbAge').value) || 0;
if (!w || !h || !a) { Q('#tmbResult').innerHTML = ''; return; }
var t = tmbSex === 'M' ? Math.round(88.362 + 13.397 * w + 4.799 * h - 5.677 * a) : Math.round(447.593 + 9.247 * w + 3.098 * h - 4.330 * a);
var fs = [{ l: 'Sedentário', m: 1.2 }, { l: 'Leve (1-3x)', m: 1.375 }, { l: 'Moderado (3-5x)', m: 1.55 }, { l: 'Intenso (6-7x)', m: 1.725 }, { l: 'Muito intenso', m: 1.9 }];
var r = '';
fs.forEach(function (f) { r += '' + f.l + ' ' + Math.round(t * f.m) + ' kcal
'; });
Q('#tmbResult').innerHTML = r;
}
// ═══ CONQUISTAS ═════════════════════
async function loadConquistas() {
try {
var d = await api('achievements');
var achs = d.achievements || [];
var stats = d.stats || {};
var unlocked = achs.filter(function (a) { return a.unlocked; });
var total = achs.length;
var pct = total > 0 ? Math.round(unlocked.length / total * 100) : 0;
var h = '' +
'
🏆
' +
'
' + unlocked.length + '/' + total + '
' +
'
Conquistas desbloqueadas
' +
'
';
// Stats resumo
h += '';
var statItems = [
{ l: 'Treinos', v: stats.totalWorkouts || 0 },
{ l: 'Volume (kg)', v: stats.totalVolume || 0 },
{ l: 'Streak', v: stats.streak || 0 },
{ l: 'Cardio', v: stats.totalCardio || 0 },
{ l: 'Km total', v: stats.totalKm || 0 },
{ l: 'Refeições', v: stats.totalMeals || 0 }
];
statItems.forEach(function (s) {
h += '
';
});
h += '
';
// Por categoria
var cats = []; achs.forEach(function (a) { if (cats.indexOf(a.category) === -1) cats.push(a.category); });
cats.forEach(function (cat) {
var catAchs = achs.filter(function (a) { return a.category === cat; });
var catDone = catAchs.filter(function (a) { return a.unlocked; });
h += '' + esc(cat) + ' (' + catDone.length + '/' + catAchs.length + ')
';
catAchs.forEach(function (a) {
var done = !!a.unlocked;
h += '' +
'
' + a.emoji + ' ' +
'
' + esc(a.title) + '
' + esc(a.description) + '
' +
(done ? '
✓
'
: '
🔒
') +
'
';
});
});
Q('#conquistasContent').innerHTML = h;
} catch (e) { Q('#conquistasContent').innerHTML = 'Erro ao carregar conquistas
'; }
}
// ═══ AR LIVRE ═══════════════════════
async function loadArLivre() {
try {
var sessions = await api('cardio&limit=50');
if (!Array.isArray(sessions)) sessions = [];
// Stats
var totalSessions = sessions.length;
var totalKm = sessions.reduce(function (s, c) { return s + (parseFloat(c.distance_km) || 0); }, 0);
var totalTime = sessions.reduce(function (s, c) { return s + (parseInt(c.duration_sec) || 0); }, 0);
var totalCal = sessions.reduce(function (s, c) { return s + (parseInt(c.calories_burned) || 0); }, 0);
var sh = '' + totalSessions + '
Atividades
' +
'' + totalKm.toFixed(1) + '
Km total
' +
'' + formatDuration(totalTime) + '
Tempo total
' +
'';
Q('#cardioStats').innerHTML = sh;
// Lista
if (!sessions.length) {
Q('#cardioList').innerHTML = 'Nenhuma atividade registrada
Registre pelo app mobile com GPS
';
return;
}
var typeEmoji = { corrida: '🏃', caminhada: '🚶', ciclismo: '🚴' };
var h = '';
sessions.forEach(function (s) {
var emoji = typeEmoji[s.type] || '🏃';
var dist = parseFloat(s.distance_km) || 0;
var dur = parseInt(s.duration_sec) || 0;
var date = s.started_at ? new Date(s.started_at).toLocaleDateString('pt-BR') : '-';
var time = s.started_at ? new Date(s.started_at).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) : '';
h += '' +
'
' + emoji + ' ' +
'
' +
'
' + esc(s.name || s.type || 'Atividade') + '
' +
'
' + date + ' às ' + time + '
' +
'
' +
'
' +
'
' + dist.toFixed(2) + ' km
' +
'
' + formatDuration(dur) + '
' +
(s.avg_pace ? '
' + s.avg_pace + ' /km
' : '') +
'
';
});
Q('#cardioList').innerHTML = h;
} catch (e) { Q('#cardioList').innerHTML = ''; }
}
function formatDuration(sec) {
if (!sec) return '0:00';
var h = Math.floor(sec / 3600);
var m = Math.floor((sec % 3600) / 60);
var s = sec % 60;
if (h > 0) return h + ':' + String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');
return m + ':' + String(s).padStart(2, '0');
}
// ─── Util ────────────────────────────
function Q(sel) { return document.querySelector(sel); }
// ─── Download do APK ─────────────────
var _cachedDownloadInfo = null;
async function getDownloadInfo() {
if (_cachedDownloadInfo) return _cachedDownloadInfo;
try {
var res = await fetch('/app-version.json?t=' + Date.now(), { cache: 'no-store' });
_cachedDownloadInfo = await res.json();
} catch (e) {
_cachedDownloadInfo = { min_version: '', download_url: '' };
}
return _cachedDownloadInfo;
}
async function downloadApp() {
var info = await getDownloadInfo();
var url = info.download_url;
if (!url) { if (typeof toast === 'function') toast('Download indisponível', true); else alert('Download indisponível'); return; }
// Dispara download sem sair da página
var a = document.createElement('a');
a.href = url;
a.download = '';
a.rel = 'noopener';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
// Ao carregar a tela de login, atualiza o rótulo de versão
(async function showAppVersionLabel() {
try {
var info = await getDownloadInfo();
var label = document.getElementById('appVersionLabel');
if (label && info.min_version) label.textContent = 'v' + info.min_version + ' · Android';
} catch (_) {}
})();
// ─── Init ────────────────────────────
(async function () {
try {
var res = await fetch('index.php?action=auth/check', { headers: { Accept: 'application/json' }, credentials: 'include' });
var data = await res.json();
if (data.success && data.user) { CSRF = data.csrf; USER = data.user; showApp(); }
else { if (data.csrf) CSRF = data.csrf; showLogin(); }
} catch (e) { showLogin(); }
})();