File size: 16,412 Bytes
0dd7c09
 
 
fa1a61c
 
 
 
 
0dd7c09
fa1a61c
0dd7c09
fa1a61c
 
0dd7c09
 
 
 
 
 
 
 
 
fa1a61c
0dd7c09
 
 
fa1a61c
 
0dd7c09
 
 
 
 
fa1a61c
 
 
0dd7c09
 
 
 
 
 
fa1a61c
0dd7c09
 
 
fa1a61c
0dd7c09
fa1a61c
 
 
0dd7c09
 
 
 
 
 
 
 
 
 
 
 
 
fa1a61c
0dd7c09
 
fa1a61c
0dd7c09
 
 
fa1a61c
0dd7c09
 
 
fa1a61c
0dd7c09
 
 
 
 
fa1a61c
0dd7c09
 
 
 
fa1a61c
 
0dd7c09
 
fa1a61c
 
0dd7c09
 
fa1a61c
0dd7c09
 
 
 
 
 
 
 
 
 
 
fa1a61c
 
0dd7c09
 
fa1a61c
 
0dd7c09
 
fa1a61c
 
0dd7c09
 
 
 
 
fa1a61c
 
0dd7c09
 
fa1a61c
 
0dd7c09
 
fa1a61c
 
0dd7c09
 
fa1a61c
0dd7c09
 
fa1a61c
0dd7c09
 
 
 
 
 
 
 
 
fa1a61c
 
0dd7c09
 
fa1a61c
0dd7c09
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fa1a61c
0dd7c09
 
 
 
 
 
 
 
 
 
 
fa1a61c
0dd7c09
 
 
 
 
 
fa1a61c
0dd7c09
 
49703ac
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Osorno Runners</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<style>
*{margin:0;padding:0;box-sizing:border-box}:root{--primary:#dc143c;--primary-dark:#8b0000;--secondary:#00a65a;--danger:#dd4b39;--warning:#f39c12;--info:#00c0ef;--dark:#222d32;--sidebar:#222d32;--content:#ecf0f5;--white:#fff;--text-dark:#333;--text-light:#666;--border:#ddd}body{font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;background:var(--content);color:var(--text-dark)}.login-container{display:flex;justify-content:center;align-items:center;min-height:100vh;background:linear-gradient(135deg,var(--primary),var(--primary-dark))}.login-box{background:var(--white);padding:40px;border-radius:10px;box-shadow:0 10px 40px rgba(0,0,0,.3);max-width:400px;width:100%}.login-header{text-align:center;margin-bottom:30px}.login-header i{font-size:60px;color:var(--primary);margin-bottom:15px}.header{background:var(--primary);color:var(--white);padding:15px 20px;display:flex;justify-content:space-between;align-items:center;position:fixed;top:0;left:0;right:0;z-index:1000}.menu-toggle{background:0 0;border:none;color:var(--white);font-size:24px;cursor:pointer}.logo{font-size:24px;font-weight:700;display:flex;align-items:center;gap:10px}.sidebar{position:fixed;top:60px;left:0;width:250px;height:calc(100vh - 60px);background:var(--sidebar);color:var(--white);overflow-y:auto;transition:transform .3s;z-index:999}.sidebar.hidden{transform:translateX(-100%)}.sidebar-menu{list-style:none;padding:20px 0}.sidebar-menu a{display:flex;align-items:center;gap:15px;padding:12px 20px;color:var(--white);text-decoration:none;cursor:pointer}.sidebar-menu a:hover,.sidebar-menu a.active{background:rgba(255,255,255,.1);border-left:3px solid var(--primary)}.main-content{margin-left:250px;margin-top:60px;padding:30px;transition:margin-left .3s}.main-content.expanded{margin-left:0}.stats-container{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:20px;margin-bottom:30px}.stat-card{background:var(--white);border-radius:8px;padding:20px;box-shadow:0 2px 10px rgba(0,0,0,.1);display:flex;justify-content:space-between;align-items:center}.stat-icon{width:70px;height:70px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:30px;color:var(--white)}.stat-card.primary .stat-icon{background:var(--primary)}.stat-card.success .stat-icon{background:var(--secondary)}.stat-card.warning .stat-icon{background:var(--warning)}.stat-card.info .stat-icon{background:var(--info)}.box{background:var(--white);border-radius:8px;box-shadow:0 2px 10px rgba(0,0,0,.1);margin-bottom:30px}.box-header{background:#f7f7f7;padding:15px 20px;border-bottom:1px solid var(--border)}.box-body{padding:20px}.form-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:20px;margin-bottom:20px}.form-group{display:flex;flex-direction:column}.form-group label{font-weight:600;margin-bottom:8px;font-size:14px}.form-group input,.form-group select,.form-group textarea{padding:10px 15px;border:1px solid var(--border);border-radius:5px;font-size:14px}.form-group input:focus,.form-group select:focus{outline:0;border-color:var(--primary)}.checkbox-group{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px}.checkbox-item{display:flex;align-items:center;padding:8px;background:#f8f9fa;border-radius:5px}.section-divider{margin:30px 0 20px;padding-bottom:12px;border-bottom:3px solid var(--primary);color:var(--primary);font-size:20px;font-weight:700}.btn{padding:12px 30px;border:none;border-radius:5px;font-size:16px;font-weight:600;cursor:pointer;display:inline-flex;align-items:center;gap:10px}.btn-primary{background:var(--primary);color:var(--white)}.btn-success{background:var(--secondary);color:var(--white)}.btn-danger{background:var(--danger);color:var(--white)}.btn-sm{padding:6px 15px;font-size:14px}.btn-secondary{background:#6c757d;color:var(--white)}.plan-list{display:grid;gap:20px}.plan-card{background:var(--white);border-radius:8px;box-shadow:0 2px 10px rgba(0,0,0,.1);padding:20px}.alert{padding:15px 20px;border-radius:5px;margin-bottom:20px;display:flex;align-items:center;gap:10px}.alert-info{background:#d1ecf1;color:#0c5460;border-left:4px solid var(--info)}.hidden{display:none!important}.prep-race-item{background:#f8f9fa;padding:15px;border-radius:8px;margin-bottom:15px;border-left:4px solid var(--primary)}@media(max-width:768px){.sidebar{transform:translateX(-100%)}.main-content{margin-left:0}}
</style>
</head>
<body>
<div id="loginScreen" class="login-container">
<div class="login-box">
<div class="login-header">
<i class="fas fa-running"></i>
<h2>Osorno Runners</h2>
<p>Sistema de Planes de Entrenamiento</p>
</div>
<form id="loginForm">
<div class="form-group">
<label><i class="fas fa-user"></i> Usuario</label>
<input type="text" id="loginUsername" required placeholder="USER o ADMIN">
</div>
<div class="form-group" style="margin-top:15px">
<label><i class="fas fa-lock"></i> Contraseña</label>
<input type="password" id="loginPassword" required>
</div>
<button type="submit" class="btn btn-primary" style="width:100%;margin-top:20px;justify-content:center">
<i class="fas fa-sign-in-alt"></i> Iniciar Sesión
</button>
</form>
<div style="margin-top:20px;text-align:center;color:#666;font-size:13px">
<p><strong>Usuario:</strong> USER / <strong>Pass:</strong> 123</p>
<p><strong>Admin:</strong> ADMIN / <strong>Pass:</strong> 123</p>
</div>
</div>
</div>

<div id="appScreen" class="hidden">
<header class="header">
<div style="display:flex;align-items:center;gap:15px">
<button class="menu-toggle" onclick="toggleSidebar()"><i class="fas fa-bars"></i></button>
<div class="logo"><i class="fas fa-running"></i><span>Osorno Runners</span></div>
</div>
<div style="display:flex;align-items:center;gap:20px">
<span id="currentUserName">Usuario</span>
<button class="btn-danger" style="padding:8px 15px;border:none;border-radius:5px;cursor:pointer" onclick="logout()">
<i class="fas fa-sign-out-alt"></i> Salir
</button>
</div>
</header>

<nav class="sidebar" id="sidebar">
<ul class="sidebar-menu">
<li><a href="#" class="active" onclick="showSection('dashboard')"><i class="fas fa-tachometer-alt"></i><span>Dashboard</span></a></li>
<li id="menu-new-plan"><a href="#" onclick="showSection('new-plan')"><i class="fas fa-plus-circle"></i><span>Nuevo Plan</span></a></li>
<li><a href="#" onclick="showSection('plan-list')"><i class="fas fa-list-ul"></i><span>Ver Planes</span></a></li>
</ul>
</nav>

<main class="main-content" id="mainContent">
<section id="dashboard-section">
<h1 style="margin-bottom:30px"><i class="fas fa-tachometer-alt"></i> Dashboard</h1>
<div class="stats-container">
<div class="stat-card primary">
<div><h3 style="font-size:14px;color:#666;margin-bottom:10px">PLANES CREADOS</h3><p id="stat-plans" style="font-size:28px;font-weight:700">0</p></div>
<div class="stat-icon"><i class="fas fa-list-alt"></i></div>
</div>
<div class="stat-card success">
<div><h3 style="font-size:14px;color:#666;margin-bottom:10px">TOTAL ATLETAS</h3><p id="stat-users" style="font-size:28px;font-weight:700">0</p></div>
<div class="stat-icon"><i class="fas fa-users"></i></div>
</div>
</div>
<div class="alert alert-info"><i class="fas fa-info-circle"></i><span>Sistema conectado a base de datos SQLite en HuggingFace Space</span></div>
</section>

<section id="new-plan-section" class="hidden">
<h1 style="margin-bottom:30px"><i class="fas fa-plus-circle"></i> Crear Nuevo Plan</h1>
<div class="box">
<div class="box-header"><h3 style="font-size:18px;font-weight:700"><i class="fas fa-user-circle"></i> Información del Atleta</h3></div>
<div class="box-body">
<form id="trainingForm">
<div class="form-grid">
<div class="form-group">
<label><i class="fas fa-user"></i> Nombre *</label>
<input type="text" id="athleteName" required>
</div>
<div class="form-group">
<label><i class="fas fa-birthday-cake"></i> Edad *</label>
<input type="number" id="athleteAge" required min="10" max="100">
</div>
<div class="form-group">
<label><i class="fas fa-layer-group"></i> Experiencia *</label>
<select id="experience" required>
<option value="">Selecciona...</option>
<option value="principiante">Principiante</option>
<option value="intermedio">Intermedio</option>
<option value="avanzado">Avanzado</option>
</select>
</div>
</div>
<div class="section-divider"><i class="fas fa-weight"></i> Antropometría</div>
<div class="form-grid">
<div class="form-group">
<label>Peso (kg) *</label>
<input type="number" id="weight" required min="30" max="200" step="0.1">
</div>
<div class="form-group">
<label>Talla (cm) *</label>
<input type="number" id="height" required min="100" max="250">
</div>
<div class="form-group">
<label>IMC</label>
<input type="text" id="imc" readonly style="background:#e9ecef">
</div>
</div>
<div class="section-divider"><i class="fas fa-heartbeat"></i> Datos Fisiológicos</div>
<div class="form-grid">
<div class="form-group">
<label>FC Máxima (bpm) *</label>
<input type="number" id="hrMax" required min="100" max="220">
</div>
<div class="form-group">
<label>FC Reposo (bpm) *</label>
<input type="number" id="hrRest" required min="30" max="100">
</div>
<div class="form-group">
<label>VO2Max (opcional)</label>
<input type="number" id="vo2max" min="20" max="90" step="0.1">
</div>
</div>
<div class="section-divider"><i class="fas fa-bullseye"></i> Objetivo</div>
<div class="form-grid">
<div class="form-group">
<label>Distancia *</label>
<select id="distance" required>
<option value="">Selecciona...</option>
<option value="5K">5K</option>
<option value="10K">10K</option>
<option value="21K">21K</option>
<option value="42K">42K</option>
</select>
</div>
<div class="form-group">
<label>Ritmo (min/km) *</label>
<input type="text" id="targetPace" required placeholder="4:30">
</div>
<div class="form-group">
<label>Fecha Carrera *</label>
<input type="date" id="raceDate" required>
</div>
</div>
<div class="section-divider"><i class="fas fa-calendar-day"></i> Disponibilidad</div>
<div class="form-group">
<label>Días de Entrenamiento *</label>
<div class="checkbox-group">
<label class="checkbox-item"><input type="checkbox" name="trainingDays" value="lunes">Lunes</label>
<label class="checkbox-item"><input type="checkbox" name="trainingDays" value="martes">Martes</label>
<label class="checkbox-item"><input type="checkbox" name="trainingDays" value="miércoles">Miércoles</label>
<label class="checkbox-item"><input type="checkbox" name="trainingDays" value="jueves">Jueves</label>
<label class="checkbox-item"><input type="checkbox" name="trainingDays" value="viernes">Viernes</label>
<label class="checkbox-item"><input type="checkbox" name="trainingDays" value="sábado">Sábado</label>
<label class="checkbox-item"><input type="checkbox" name="trainingDays" value="domingo">Domingo</label>
</div>
</div>
<div style="margin-top:30px">
<button type="submit" class="btn btn-success"><i class="fas fa-check"></i> Generar Plan</button>
</div>
</form>
</div>
</div>
</section>

<section id="plan-list-section" class="hidden">
<h1 style="margin-bottom:30px"><i class="fas fa-list-ul"></i> Planes de Entrenamiento</h1>
<div id="planListContent" class="plan-list"></div>
<div id="noPlanPlaceholder" class="hidden">
<div class="alert alert-info"><i class="fas fa-info-circle"></i><span>No hay planes disponibles</span></div>
</div>
</section>
</main>
</div>

<script>
let currentUser=null;const API_URL='/api';async function apiCall(endpoint,data){try{const response=await fetch(API_URL+endpoint,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});return await response.json()}catch(e){console.error('API Error:',e);return{success:false,error:e.message}}}document.getElementById('loginForm').addEventListener('submit',async function(e){e.preventDefault();const username=document.getElementById('loginUsername').value.toUpperCase(),password=document.getElementById('loginPassword').value,result=await apiCall('/login',{username:username,password:password});if(result.success){currentUser=result.user;document.getElementById('loginScreen').classList.add('hidden');document.getElementById('appScreen').classList.remove('hidden');document.getElementById('currentUserName').textContent=currentUser.name;currentUser.role==='user'&&(document.getElementById('menu-new-plan').style.display='none');loadPlans()}else alert('Usuario o contraseña incorrectos')});function logout(){currentUser=null;document.getElementById('appScreen').classList.add('hidden');document.getElementById('loginScreen').classList.remove('hidden');document.getElementById('loginForm').reset()}function toggleSidebar(){document.getElementById('sidebar').classList.toggle('hidden');document.getElementById('mainContent').classList.toggle('expanded')}function showSection(section){document.querySelectorAll('main > section').forEach(s=>s.classList.add('hidden'));document.querySelectorAll('.sidebar-menu a').forEach(a=>a.classList.remove('active'));if('dashboard'===section)document.getElementById('dashboard-section').classList.remove('hidden');else if('new-plan'===section){if(currentUser&&'admin'===currentUser.role)document.getElementById('new-plan-section').classList.remove('hidden');else{alert('Solo administradores pueden crear planes');return}}else'plan-list'===section&&(document.getElementById('plan-list-section').classList.remove('hidden'),loadPlans());window.event&&window.event.target&&window.event.target.closest('a').classList.add('active')}async function loadPlans(){const result=await apiCall('/get_plans',{username:currentUser.username,role:currentUser.role});if(result.success){const plans=result.plans,listContent=document.getElementById('planListContent'),placeholder=document.getElementById('noPlanPlaceholder');if(0===plans.length){placeholder.classList.remove('hidden');listContent.innerHTML='';return}placeholder.classList.add('hidden');let html='';plans.forEach(plan=>{html+=`<div class="plan-card"><div><h3><i class="fas fa-user"></i> ${plan.userData.name}</h3><p><strong>Objetivo:</strong> ${plan.userData.distance} - ${plan.totalWeeks} sem</p></div><div style="display:flex;gap:10px"><button class="btn btn-success btn-sm" onclick="generatePDF(${plan.id})"><i class="fas fa-file-pdf"></i> PDF</button>${currentUser&&'admin'===currentUser.role?`<button class="btn btn-danger btn-sm" onclick="deletePlan(${plan.id})"><i class="fas fa-trash"></i></button>`:''}</div></div>`});listContent.innerHTML=html;document.getElementById('stat-plans').textContent=plans.length}}document.getElementById('trainingForm').addEventListener('submit',async function(e){e.preventDefault();const trainingDays=Array.from(document.querySelectorAll('input[name="trainingDays"]:checked')).map(cb=>cb.value);if(trainingDays.length<3)return void alert('Selecciona al menos 3 días');const plan={userData:{name:document.getElementById('athleteName').value,age:parseInt(document.getElementById('athleteAge').value),experience:document.getElementById('experience').value,weight:parseFloat(document.getElementById('weight').value),height:parseFloat(document.getElementById('height').value),imc:document.getElementById('imc').value,hrMax:parseInt(document.getElementById('hrMax').value),hrRest:parseInt(document.getElementById('hrRest').value),vo2max:document.getElementById('vo2max').value||null,distance:document.getElementById('distance').value,targetPace:document.getElementById('targetPace').value,raceDate:document.getElementById('raceDate').value,trainingDays:trainingDays},totalWeeks:12,weeklyPlans:[]};const result=await apiCall('/save_plan',{plan:JSON.stringify(plan),username:currentUser.username});result.success?(alert('Plan creado exitosamente'),document.getElementById('trainingForm').reset(),showSection('plan-list')):alert('Error al crear plan: '+result.error)});async function deletePlan(planId){if(confirm('¿Eliminar este plan?')){const result=await apiCall('/delete_plan',{plan_id:planId});result.success?(alert('Plan eliminado'),loadPlans()):alert('Error al eliminar')}}function generatePDF(planId){alert('Generación de PDF disponible en versión completa')}
</script>
</body>
</html>