first commit. application working

This commit is contained in:
Jaime Jiménez
2025-07-15 18:34:27 +02:00
parent bc830ef927
commit 5099ab0607
3745 changed files with 360739 additions and 3 deletions

View File

@ -0,0 +1,198 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dashboard | Weight Tracker</title>
<link th:href="@{/vendor/fontawesome-free/css/all.min.css}" rel="stylesheet">
<link th:href="@{/css/sb-admin-2.min.css}" rel="stylesheet">
<link rel="icon" href="data:,">
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<!-- Daterangepicker -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.css" />
</head>
<body id="page-top">
<div id="wrapper">
<div th:replace="~{fragments/fragment-menu :: menu}"></div>
<div id="content-wrapper" class="d-flex flex-column">
<div id="content" class="p-4">
<nav class="navbar navbar-expand navbar-light bg-white topbar mb-4 static-top shadow">
<button id="sidebarToggleTop" class="btn btn-link d-md-none rounded-circle mr-3">
<i class="fa fa-bars"></i>
</button>
<h1 class="h4 mb-0 text-gray-800">Dashboard</h1>
</nav>
<div class="container-fluid">
<div class="row gx-3 gy-4">
<!-- Últimos registros -->
<div class="col-12 col-lg-6">
<div class="card shadow mb-4 h-100">
<div class="card-header">Últimos 10 registros</div>
<div class="card-body">
<table class="table table-sm">
<thead>
<tr>
<th>Fecha</th>
<th>Peso (kg)</th>
</tr>
</thead>
<tbody>
<tr th:each="r : ${ultimos}">
<td th:text="${#temporals.format(r.date, 'yyyy-MM-dd HH:mm')}">2025-07-10</td>
<td th:text="${r.weight}">70.5</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Gráfica -->
<div class="col-12 col-lg-6 mt-3 mt-lg-0">
<div class="card shadow mb-4 h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<span id="chart-title">Evolución del peso</span>
<div class="dropdown">
<button class="btn btn-sm btn-secondary dropdown-toggle" type="button" id="metricDropdown"
data-bs-toggle="dropdown" aria-expanded="false">
Métricas
</button>
<ul class="dropdown-menu dropdown-menu-end p-2" aria-labelledby="metricDropdown"
style="min-width: 200px;">
<li><div class="form-check"><input class="form-check-input metric-check" type="checkbox" value="weight" id="metricWeight" checked><label class="form-check-label" for="metricWeight">Peso (kg)</label></div></li>
<li><div class="form-check"><input class="form-check-input metric-check" type="checkbox" value="bodyFat" id="metricBodyFat"><label class="form-check-label" for="metricBodyFat">% Grasa</label></div></li>
<li><div class="form-check"><input class="form-check-input metric-check" type="checkbox" value="muscleMass" id="metricMuscleMass"><label class="form-check-label" for="metricMuscleMass">Masa muscular</label></div></li>
<li><div class="form-check"><input class="form-check-input metric-check" type="checkbox" value="waterPercent" id="metricWater"><label class="form-check-label" for="metricWater">% Agua</label></div></li>
<li><div class="form-check"><input class="form-check-input metric-check" type="checkbox" value="bmi" id="metricBMI"><label class="form-check-label" for="metricBMI">IMC</label></div></li>
<li><div class="form-check"><input class="form-check-input metric-check" type="checkbox" value="metabolicAge" id="metricMetAge"><label class="form-check-label" for="metricMetAge">Edad metabólica</label></div></li>
<li><div class="form-check"><input class="form-check-input metric-check" type="checkbox" value="visceralFat" id="metricVisceral"><label class="form-check-label" for="metricVisceral">Grasa visceral</label></div></li>
</ul>
</div>
</div>
<div class="card-body">
<div class="mb-2">
<label for="chartDateRange">Filtrar por fecha:</label>
<input type="text" id="chartDateRange" class="form-control form-control-sm w-auto d-inline-block">
</div>
<canvas id="chart" class="w-100" style="max-height: 300px;"></canvas>
</div>
</div>
</div>
</div>
</div>
<!-- jQuery, Bootstrap y plugins -->
<script th:src="@{/vendor/jquery/jquery.min.js}"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/moment@2.29.4/moment.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.min.js"></script>
<script th:src="@{/js/sb-admin-2.min.js}"></script>
<!-- Script Chart -->
<script th:inline="javascript">
$(document).ready(function () {
// Datos desde el controlador
const labels = /*[[${labels}]]*/[];
const dataMap = {
weight: /*[[${pesos}]]*/[],
bodyFat: /*[[${grasa}]]*/[],
muscleMass: /*[[${musculo}]]*/[],
waterPercent: /*[[${agua}]]*/[],
bmi: /*[[${bmi}]]*/[],
metabolicAge: /*[[${edadMetabolica}]]*/[],
visceralFat: /*[[${grasaVisceral}]]*/[]
};
const metricColors = {
weight: 'rgba(78, 115, 223, 1)',
bodyFat: 'rgba(231, 74, 59, 1)',
muscleMass: 'rgba(28, 200, 138, 1)',
waterPercent: 'rgba(54, 185, 204, 1)',
bmi: 'rgba(246, 194, 62, 1)',
metabolicAge: 'rgba(133, 135, 150, 1)',
visceralFat: 'rgba(106, 90, 205, 1)'
};
const ctx = document.getElementById('chart').getContext('2d');
const chart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'Peso (kg)',
data: dataMap.weight,
borderColor: metricColors.weight,
borderWidth: 2,
fill: false,
tension: 0.3,
pointRadius: 3
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
ticks: {
maxRotation: 45,
minRotation: 45
}
},
y: {
beginAtZero: false
}
}
}
});
// Filtro por métricas
$('.metric-check').on('change', function () {
const selected = $('.metric-check:checked').map(function () {
return this.value;
}).get();
chart.data.datasets = selected.map(metric => ({
label: $(`label[for="metric${metric.charAt(0).toUpperCase() + metric.slice(1)}"]`).text(),
data: dataMap[metric],
borderColor: metricColors[metric],
borderWidth: 2,
fill: false,
tension: 0.3,
pointRadius: 3
}));
chart.update();
});
// Rango de fechas
$('#chartDateRange').daterangepicker({
autoUpdateInput: false,
locale: {
cancelLabel: 'Limpiar',
applyLabel: 'Aplicar',
format: 'YYYY-MM-DD'
}
});
$('#chartDateRange').on('apply.daterangepicker', function (ev, picker) {
const start = picker.startDate.format('YYYY-MM-DD');
const end = picker.endDate.format('YYYY-MM-DD');
window.location.href = `/?start=${start}&end=${end}`;
});
$('#chartDateRange').on('cancel.daterangepicker', function () {
window.location.href = `/`;
});
});
</script>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,51 @@
<!-- src/main/resources/templates/fragments/fragment-menu.html -->
<ul class="navbar-nav bg-gradient-primary sidebar sidebar-dark accordion toggled" id="accordionSidebar" th:fragment="menu">
<!-- Sidebar - Brand -->
<a class="sidebar-brand d-flex align-items-center justify-content-center" th:href="@{/}">
<div class="sidebar-brand-icon">
<i class="fas fa-weight"></i>
</div>
<div class="sidebar-brand-text mx-3">Weight Tracker</div>
</a>
<!-- Divider -->
<hr class="sidebar-divider my-0">
<!-- Nav Items -->
<li class="nav-item">
<a class="nav-link" th:href="@{/registros}">
<i class="fas fa-list"></i>
<span>Ver registros</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" th:href="@{/registros/nuevo}">
<i class="fas fa-plus"></i>
<span>Añadir registro</span>
</a>
</li>
<li class="nav-item" th:if="${#authorization.expression('hasRole(''ADMIN'')')}">
<a class="nav-link" th:href="@{/usuarios}">
<i class="fas fa-users-cog"></i>
<span>Usuarios</span>
</a>
</li>
<!-- Logout -->
<li class="nav-item">
<form th:action="@{/logout}" method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<button type="submit" class="nav-link btn btn-link text-start">
<i class="fas fa-sign-out-alt"></i>
<span>Cerrar sesión</span>
</button>
</form>
</li>
<!-- Sidebar Toggler (hamburguesa) -->
<div class="text-center d-md-inline">
<button class="rounded-circle border-0" id="sidebarToggle"></button>
</div>
</ul>

View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Login | Weight Tracker</title>
<link th:href="@{/css/sb-admin-2.min.css}" rel="stylesheet">
</head>
<body class="bg-gradient-primary d-flex align-items-center" style="height:100vh;">
<div class="container">
<div class="row justify-content-center">
<div class="col-md-4">
<div class="card shadow">
<div class="card-body">
<h4 class="text-center mb-3">Iniciar sesión</h4>
<form th:action="@{/do-login}" method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<div class="mb-3">
<input class="form-control" name="username" placeholder="Usuario" required>
</div>
<div class="mb-3">
<input class="form-control" type="password" name="password" placeholder="Contraseña" required>
</div>
<button class="btn btn-primary w-100" type="submit">Entrar</button>
</form>
<div class="text-danger mt-2" th:if="${param.error}">Credenciales incorrectas</div>
<div class="text-success mt-2" th:if="${param.logout}">Sesión cerrada</div>
</div>
</div>
</div>
</div>
</div>
<script th:src="@{/js/sb-admin-2.min.js}" defer></script>
</body>
</html>

View File

@ -0,0 +1,110 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Añadir registro | Weight Tracker</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link th:href="@{/vendor/fontawesome-free/css/all.min.css}" rel="stylesheet">
<link th:href="@{/css/sb-admin-2.min.css}" rel="stylesheet">
<link rel="icon" href="data:,">
<!-- Flatpickr CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/plugins/confirmDate/confirmDate.css">
</head>
<body id="page-top">
<div id="wrapper">
<div th:replace="~{fragments/fragment-menu :: menu}"></div>
<div id="content-wrapper" class="d-flex flex-column">
<div id="content" class="p-4">
<nav class="navbar navbar-expand navbar-light bg-white topbar mb-4 static-top shadow">
<button id="sidebarToggleTop" class="btn btn-link d-md-none rounded-circle mr-3">
<i class="fa fa-bars"></i>
</button>
<h1 class="h4 mb-0 text-gray-800">Añadir registro</h1>
</nav>
<form th:action="@{/registros/guardar}" th:object="${registro}" method="post" class="row g-3">
<input type="hidden" th:field="*{id}" />
<div class="col-12 col-md-6">
<label class="form-label">Fecha y hora</label>
<input type="datetime-local" th:field="*{date}" class="form-control" required />
</div>
<div class="col-12 col-md-6">
<label class="form-label">Peso (kg)</label>
<input type="number" step="0.1" th:field="*{weight}" class="form-control" />
</div>
<div class="col-12 col-md-6">
<label class="form-label">% Grasa corporal</label>
<input type="number" step="0.1" th:field="*{bodyFat}" class="form-control" />
</div>
<div class="col-12 col-md-6">
<label class="form-label">% Agua</label>
<input type="number" step="0.1" th:field="*{waterPercent}" class="form-control" />
</div>
<div class="col-12 col-md-6">
<label class="form-label">Masa muscular (kg)</label>
<input type="number" step="0.1" th:field="*{muscleMass}" class="form-control" />
</div>
<div class="col-12 col-md-6">
<label class="form-label">Edad metabólica</label>
<input type="number" th:field="*{metabolicAge}" class="form-control" />
</div>
<div class="col-12 col-md-6">
<label class="form-label">Grasa visceral</label>
<input type="number" th:field="*{visceralFat}" class="form-control" />
</div>
<div class="col-12 col-md-6">
<label class="form-label">IMC (BMI)</label>
<input type="number" step="0.1" th:field="*{bmi}" class="form-control" />
</div>
<div class="col-12">
<label class="form-label">Notas</label>
<textarea th:field="*{notes}" class="form-control" rows="3" placeholder="Observaciones opcionales..."></textarea>
</div>
<div class="col-12">
<button type="submit" class="btn btn-primary mt-3">Guardar</button>
</div>
</form>
</div>
</div>
</div>
<!-- Flatpickr scripts -->
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/plugins/confirmDate/confirmDate.js"></script>
<script>
flatpickr("input[type=datetime-local]", {
enableTime: true,
time_24hr: true,
dateFormat: "Y-m-d\\TH:i",
plugins: [new confirmDatePlugin({
confirmText: "OK",
showAlways: false,
theme: "light",
confirmIcon: "",
showNow: true,
nowText: "Ahora"
})]
});
</script>
<script th:src="@{/vendor/jquery/jquery.min.js}"></script>
<script th:src="@{/vendor/bootstrap/js/bootstrap.bundle.min.js}"></script>
<script th:src="@{/js/sb-admin-2.min.js}"></script>
</body>
</html>

View File

@ -0,0 +1,121 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Registros | Weight Tracker</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link th:href="@{/vendor/fontawesome-free/css/all.min.css}" rel="stylesheet">
<link th:href="@{/css/sb-admin-2.min.css}" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/jquery.dataTables.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.css" />
<link rel="icon" href="data:,">
</head>
<body id="page-top">
<div id="wrapper">
<div th:replace="~{fragments/fragment-menu :: menu}"></div>
<div id="content-wrapper" class="d-flex flex-column">
<div id="content" class="p-4">
<nav class="navbar navbar-expand navbar-light bg-white topbar mb-4 static-top shadow">
<button id="sidebarToggleTop" class="btn btn-link d-md-none rounded-circle mr-3">
<i class="fa fa-bars"></i>
</button>
<h1 class="h4 mb-0 text-gray-800">Registros</h1>
</nav>
<div class="container-fluid">
<a class="btn btn-primary mb-3" th:href="@{/registros/nuevo}">Nuevo</a>
<div class="mb-2">
<label>Filtrar por fecha:</label>
<input type="text" id="dateRange" class="form-control form-control-sm w-auto d-inline-block">
</div>
<div class="table-responsive">
<table id="tablaRegistros" class="table table-bordered table-striped align-middle">
<thead class="table-light">
<tr>
<th>Fecha</th>
<th>Peso (kg)</th>
<th>% Grasa</th>
<th>Masa muscular (kg)</th>
<th>% Agua</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
<tr th:each="r : ${registros}">
<td th:text="${#temporals.format(r.date, 'yyyy-MM-dd HH:mm')}">2025-07-10 18:00</td>
<td th:text="${r.weight}">70.5</td>
<td th:text="${r.bodyFat}">15.0</td>
<td th:text="${r.muscleMass}">55.0</td>
<td th:text="${r.waterPercent}">60.0</td>
<td>
<a class="btn btn-sm btn-outline-primary" th:href="@{'/registros/' + ${r.id} + '/editar'}">Editar</a>
<form th:action="@{'/registros/' + ${r.id} + '/eliminar'}" method="post" class="d-inline">
<input type="hidden" name="_method" value="delete" />
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('¿Eliminar registro?')">Eliminar</button>
</form>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Scripts -->
<script th:src="@{/vendor/jquery/jquery.min.js}"></script>
<script th:src="@{/vendor/bootstrap/js/bootstrap.bundle.min.js}"></script>
<script th:src="@{/js/sb-admin-2.min.js}"></script>
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.jsdelivr.net/momentjs/latest/moment.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.min.js"></script>
<script th:inline="none">
$(document).ready(function () {
const table = $('#tablaRegistros').DataTable({
order: [[0, 'desc']],
pageLength: 10,
searching: false,
lengthMenu: [5, 10, 25, 50, 100],
language: {
url: 'https://cdn.datatables.net/plug-ins/2.3.2/i18n/es-ES.json',
}
});
// Rango de fechas
let startDate, endDate;
$('#dateRange').daterangepicker({
autoUpdateInput: false,
locale: {
cancelLabel: 'Limpiar',
applyLabel: 'Aplicar',
format: 'YYYY-MM-DD'
}
});
$('#dateRange').on('apply.daterangepicker', function (ev, picker) {
startDate = picker.startDate;
endDate = picker.endDate;
$(this).val(startDate.format('YYYY-MM-DD') + ' - ' + endDate.format('YYYY-MM-DD'));
table.draw();
});
$('#dateRange').on('cancel.daterangepicker', function (ev, picker) {
$(this).val('');
startDate = endDate = null;
table.draw();
});
$.fn.dataTable.ext.search.push(function (settings, data, dataIndex) {
const fecha = moment(data[0], 'YYYY-MM-DD HH:mm');
if (!startDate || !endDate) return true;
return fecha.isBetween(startDate, endDate, 'day', '[]');
});
});
</script>
</body>
</html>

View File

@ -0,0 +1,64 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<link th:href="@{/vendor/fontawesome-free/css/all.min.css}" rel="stylesheet">
<link th:href="@{/css/sb-admin-2.min.css}" rel="stylesheet">
<link rel="icon" href="data:,">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body id="page-top">
<div id="wrapper">
<div th:replace="~{fragments/fragment-menu :: menu}"></div>
<div id="content-wrapper" class="d-flex flex-column">
<div id="content" class="p-4">
<nav class="navbar navbar-expand navbar-light bg-white topbar mb-4 static-top shadow">
<!-- Botón hamburguesa solo visible en móvil -->
<button id="sidebarToggleTop" class="btn btn-link d-md-none rounded-circle mr-3">
<i class="fa fa-bars"></i>
</button>
<h1 class="h4 mb-0 text-gray-800">Formulario de usuario</h1>
</nav>
<div class="container-fluid">
<h2 class="mb-4" th:text="${usuario.id} != null ? 'Editar usuario' : 'Nuevo usuario'">Nuevo usuario</h2>
<form th:action="@{/usuarios/guardar}" th:object="${usuario}" method="post" class="row g-3">
<input type="hidden" th:field="*{id}" />
<div class="col-12 col-md-6">
<label class="form-label">Usuario</label>
<input type="text" th:field="*{username}" class="form-control" required />
</div>
<div class="col-12 col-md-6">
<label class="form-label">Contraseña</label>
<input type="password" th:field="*{password}" class="form-control" th:placeholder="${usuario.id} != null ? '••••••••' : ''" />
</div>
<div class="col-12 col-md-6">
<label class="form-label">Rol</label>
<select th:field="*{role}" class="form-select">
<option value="USER">Usuario</option>
<option value="ADMIN">Administrador</option>
</select>
</div>
<div class="col-12">
<button type="submit" class="btn btn-primary mt-3">Guardar</button>
</div>
</form>
</div>
</div>
</div>
</div>
<script th:src="@{/vendor/jquery/jquery.min.js}"></script>
<script th:src="@{/vendor/bootstrap/js/bootstrap.bundle.min.js}"></script>
<script th:src="@{/js/sb-admin-2.min.js}"></script>
</body>
</html>

View File

@ -0,0 +1,67 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<link th:href="@{/vendor/fontawesome-free/css/all.min.css}" rel="stylesheet">
<link th:href="@{/css/sb-admin-2.min.css}" rel="stylesheet">
<link rel="icon" href="data:,">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body id="page-top">
<div id="wrapper">
<div th:replace="~{fragments/fragment-menu :: menu}"></div>
<div id="content-wrapper" class="d-flex flex-column">
<div id="content" class="p-4">
<nav class="navbar navbar-expand navbar-light bg-white topbar mb-4 static-top shadow">
<!-- Botón hamburguesa solo visible en móvil -->
<button id="sidebarToggleTop" class="btn btn-link d-md-none rounded-circle mr-3">
<i class="fa fa-bars"></i>
</button>
<h1 class="h4 mb-0 text-gray-800">Usuarios</h1>
</nav>
<div class="container-fluid">
<a class="btn btn-primary mb-3" th:href="@{/usuarios/nuevo}">Nuevo</a>
<div class="table-responsive">
<table class="table table-bordered table-striped align-middle">
<thead class="table-light">
<tr>
<th>Usuario</th>
<th>Rol</th>
<th></th>
</tr>
</thead>
<tbody>
<tr th:each="u : ${usuarios}">
<td th:text="${u.username}">usuario</td>
<td th:text="${u.role}">ROLE_USER</td>
<td>
<a class="btn btn-sm btn-outline-primary" th:href="@{'/usuarios/' + ${u.id} + '/editar'}">Editar</a>
<form th:action="@{'/usuarios/' + ${u.id} + '/eliminar'}" method="post" class="d-inline"
onsubmit="return confirm('¿Eliminar este usuario y sus registros?');">
<input type="hidden" name="_method" value="delete" />
<button class="btn btn-sm btn-outline-danger" type="submit">Eliminar</button>
</form>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<script th:src="@{/vendor/jquery/jquery.min.js}"></script>
<script th:src="@{/vendor/bootstrap/js/bootstrap.bundle.min.js}"></script>
<script th:src="@{/js/sb-admin-2.min.js}"></script>
</body>
</html>