// ═══════════════════════════════════════ // 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 ? '
Nenhuma rotina
' : ''); } 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 = ''; h += ''; 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 = ''; h += '

' + esc(r.name) + '

'; if (r.description) h += '

' + esc(r.description) + '

'; if (r.exercises && r.exercises.length) { r.exercises.forEach(function (ex, i) { h += '
' + (i + 1) + '
' + esc(ex.name) + '' + ex.sets_target + '×' + ex.reps_target + ' · ' + ex.rest_seconds + 's
'; if (ex.notes) h += '
' + esc(ex.notes) + '
'; h += '
'; }); } else { h += '
Nenhum exercício
'; } h += '
'; 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 += '
'; activeSession.exercises.forEach(function (ex, ei) { h += '
' + (ei + 1) + '
' + esc(ex.name) + '' + ex.sets_target + '×' + ex.reps_target + '
'; h += ''; ex.sets.forEach(function (set, si) { h += ''; }); h += '
SETKGREPS
' + (si + 1) + '
+ Série
'; }); 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 = ''; gs.forEach(function (g) { sel.innerHTML += ''; }); } 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 = '
Erro ao carregar
'; } } 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 = ''; 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 += ''; 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 = '
Sem refeições
'; } 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 += '
EditarRemover
'; }); Q('#mealsList').innerHTML = mh; } // Total Q('#nutriTotal').style.display = ''; Q('#nutriTotal').innerHTML = '
Total
' + tc + '
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 = '
Sem histórico
'; 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 += ''; }); h += '
'; return h; } function setMeasField(k) { _measChartField = k; loadMeasurements(); } async function loadMeasurements() { try { var data = await api('measurements'); if (!data.length) { Q('#measurementsList').innerHTML = '
Sem medições
'; 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 = '
Erro
'; } } 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 = ''; h += '
' + [7, 14, 30].map(function (p) { return '
' + p + 'd
'; }).join('') + '
'; h += '
' + avgCal + '
kcal/dia média
' + avgW + '
ml água/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 + 'kcalP' + Math.round(d.protein) + 'g' + d.water_ml + 'ml' + (met ? ' ✓' : '') + '
'; }); Q('#nutriHistoryContent').innerHTML = h; } catch (e) { Q('#nutriHistoryContent').innerHTML = '
Erro ao carregar
'; } } // ═══ 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 = ''; h += '

Planos

'; 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
' + '
' + '' + '' + '
'; }); 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 = '
Erro
'; } } 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 += '
Crie um plano primeiro
'; 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 += '
'; }); h += '
'; 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 = '
TMB
' + t + ' kcal/dia
'; 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 += '
' + s.v + '
' + s.l + '
'; }); 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
' + '
' + totalCal + '
Calorias
'; 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 = '
Erro ao carregar
'; } } 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(); } })();