implementados filtros y delete. falta los textos del delete

This commit is contained in:
2025-09-28 09:41:15 +02:00
parent 847249d2de
commit 50599cf33e
5 changed files with 163 additions and 41 deletions

View File

@ -7,7 +7,10 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.context.MessageSource;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
@ -28,6 +31,7 @@ import com.imprimelibros.erp.config.Sanitizer;
import com.imprimelibros.erp.datatables.DataTable;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.List;
@ -75,7 +79,8 @@ public class UserController {
// Si 'role' es relación, sácalo de aquí:
List<String> searchable = List.of("fullName", "userName", "enabled", "rolesConcat"); // <- busca por roles de
// verdad
List<String> orderable = List.of("fullName", "userName", "enabled", "roleRank"); // <- permite ordenar por estas columnas
List<String> orderable = List.of("fullName", "userName", "enabled", "roleRank"); // <- permite ordenar por estas
// columnas
Specification<User> base = (root, query, cb) -> cb.conjunction();
long total = repo.count();
@ -83,7 +88,7 @@ public class UserController {
return DataTable
.of(repo, User.class, dt, searchable) // 'searchable' en DataTable.java
// edita columnas "reales":
.orderable(orderable)
.orderable(orderable)
.edit("enabled", (User u) -> {
if (u.isEnabled()) {
return "<span class=\"badge bg-success\" >"
@ -105,10 +110,30 @@ public class UserController {
" <a href=\"javascript:void(0);\" data-id=\"" + user.getId()
+ "\" class=\"link-success btn-edit-user fs-15\"><i class=\"ri-edit-2-line\"></i></a>\n" +
" <a href=\"javascript:void(0);\" data-id=\"" + user.getId()
+ "\" class=\"link-danger fs-15\"><i class=\"user-delete ri-delete-bin-line\"></i></a>\n" +
+ "\" class=\"link-danger btn-delete-user fs-15\"><i class=\"user-delete ri-delete-bin-line\"></i></a>\n" +
" </div>";
})
.where(base)
// Filtros custom:
.filter((builder, req) -> {
// f_enabled: 'true' | 'false' | ''
String fEnabled = Optional.ofNullable(req.raw.get("f_enabled")).orElse("").trim();
if (!fEnabled.isEmpty()) {
boolean enabledVal = Boolean.parseBoolean(fEnabled);
builder.add((root, q, cb) -> cb.equal(root.get("enabled"), enabledVal));
}
// f_role: 'USER' | 'ADMIN' | 'SUPERADMIN' | ''
String fRole = Optional.ofNullable(req.raw.get("f_role")).orElse("").trim();
if (!fRole.isEmpty()) {
builder.add((root, q, cb) -> {
// join a roles; marca la query como distinct para evitar duplicados
var r = root.join("roles", jakarta.persistence.criteria.JoinType.LEFT);
q.distinct(true);
return cb.equal(r.get("name"), fRole);
});
}
})
.toJson(total);
}
@ -255,33 +280,28 @@ public class UserController {
}
@DeleteMapping("/{id}")
@ResponseBody
public void delete(@PathVariable Long id, HttpServletResponse response, Authentication authentication) {
var uOpt = repo.findById(id);
if (uOpt.isEmpty()) {
response.setStatus(404);
return;
}
var u = uOpt.get();
String currentUserName = authentication.getName();
if (u.getUserName().equalsIgnoreCase(currentUserName)) {
response.setStatus(403); // no puede borrarse a sí mismo
return;
}
try {
repo.delete(u);
} catch (Exception ex) {
response.setStatus(500);
}
// Si llegamos aquí, la eliminación fue exitosa
/*
* response.setStatus(204);
* response.getWriter().flush();
* response.getWriter().close();
*/
return;
public ResponseEntity<?> delete(@PathVariable Long id, Authentication authentication, Locale locale) {
return repo.findById(id).map(u -> {
if (authentication != null && u.getUserName().equalsIgnoreCase(authentication.getName())) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(Map.of("message", messageSource.getMessage("usuarios.error.delete-self", null, locale)));
}
try {
repo.delete(u);
return ResponseEntity.status(HttpStatus.OK).body(
Map.of("message", messageSource.getMessage("usuarios.exito.eliminado", null, locale))
);
} catch (DataIntegrityViolationException dive) {
// Restricción FK / dependencias
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(Map.of("message", messageSource.getMessage("usuarios.error.delete-relational-data", null, locale)));
} catch (Exception ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("message", messageSource.getMessage("usuarios.error.delete-internal-error", null, locale)));
}
}).orElseGet(() -> ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(Map.of("message", messageSource.getMessage("usuarios.error.delete-not-found", null, locale))));
}
}

View File

@ -5,6 +5,7 @@ usuarios.add=Añadir usuario
usuarios.eliminar=Eliminar usuario
usuarios.confirmarEliminar=¿Está seguro de que desea eliminar este usuario?
usuarios.guardar=Guardar
usuarios.todos=Todos
usuarios.tabla.id=ID
usuarios.tabla.nombre=Nombre
@ -38,6 +39,10 @@ usuarios.error.password.requerida=La contraseña es obligatoria.
usuarios.error.password.min=La contraseña debe tener al menos 6 caracteres.
usuarios.error.confirmPassword.requerida=La confirmación de la contraseña es obligatoria.
usuarios.error.password-coinciden=Las contraseñas no coinciden.
usuarios.error.delete-relational-data=No se puede eliminar el usuario porque tiene datos relacionados.
usuarios.error.delete-internal-error=No se puede eliminar: error interno.
usuarios.error.delete-not-found=No se puede eliminar: usuario no encontrado.
usuarios.error.delete-self=No se puede eliminar a sí mismo.
usuarios.exito.creado=Usuario creado con éxito.
usuarios.exito.actualizado=Usuario actualizado con éxito.

View File

@ -1,16 +1,37 @@
$(() => {
const language = document.documentElement.lang || 'es-ES';
// CSRF global para AJAX
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.getAttribute('content');
if (csrfToken && csrfHeader) {
$.ajaxSetup({
beforeSend: function (xhr) {
xhr.setRequestHeader(csrfHeader, csrfToken);
}
});
}
const table = new DataTable('#users-datatable', {
processing: true, serverSide: true, pageLength: 50,
processing: true,
serverSide: true,
orderCellsTop: true,
pageLength: 50,
language: { url: '/assets/libs/datatables/i18n/' + language + '.json' },
responsive: true,
ajax: { url: '/users/datatable', method: 'GET' },
order: [[0,'asc']],
ajax: {
url: '/users/datatable',
method: 'GET',
data: function (d) {
d.f_role = $('#search-role').val() || ''; // 'USER' | 'ADMIN' | 'SUPERADMIN' | ''
d.f_enabled = $('#search-status').val() || ''; // 'true' | 'false' | ''
}
},
order: [[0, 'asc']],
columns: [
{ data: 'id', name: 'id' , orderable: true },
{ data: 'fullName', name: 'fullName' , orderable: true },
{ data: 'userName', name: 'userName' , orderable: true },
{ data: 'id', name: 'id', orderable: true },
{ data: 'fullName', name: 'fullName', orderable: true },
{ data: 'userName', name: 'userName', orderable: true },
{ data: 'roles', name: 'roleRank' },
{ data: 'enabled', name: 'enabled', searchable: false },
{ data: 'actions', name: 'actions' }
@ -18,8 +39,21 @@ $(() => {
columnDefs: [{ targets: -1, orderable: false, searchable: false }]
});
table.on("keyup", ".user-filter", function () {
const colName = $(this).data("col");
const colIndex = table.settings()[0].aoColumns.findIndex(c => c.name === colName);
if (colIndex >= 0) {
table.column(colIndex).search(this.value).draw();
}
});
table.on("change", ".user-filter-select", function () {
table.draw();
});
const modalEl = document.getElementById('userFormModal');
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
// Abrir "Crear"
$('#addUserButton').on('click', (e) => {
@ -36,7 +70,7 @@ $(() => {
$(document).on('click', '.btn-edit-user', function (e) {
e.preventDefault();
const id = $(this).data('id');
$.get('/users/form', { id }, function (html) {
$.get('/users/form', { id }, function (html) {
$('#userModalBody').html(html);
const title = $('#userModalBody #userForm').data('edit');
$('#userFormModal .modal-title').text(title);
@ -44,6 +78,40 @@ $(() => {
});
});
// Botón "Eliminar"
$(document).on('click', '.btn-delete-user', function (e) {
e.preventDefault();
const id = $(this).data('id');
Swal.fire({
title: '¿Eliminar usuario?',
text: 'Esta acción no se puede deshacer.',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Sí, eliminar',
cancelButtonText: 'Cancelar',
reverseButtons: true
}).then((result) => {
if (!result.isConfirmed) return;
$.ajax({
url: '/users/' + id,
type: 'DELETE',
success: function () {
Swal.fire({ icon: 'success', title: 'Eliminado', timer: 1200, showConfirmButton: false });
$('#users-datatable').DataTable().ajax.reload(null, false);
},
error: function (xhr) {
// usa el mensaje del backend; fallback genérico por si no llega JSON
const msg = (xhr.responseJSON && xhr.responseJSON.message)
|| 'Error al eliminar el usuario.';
Swal.fire({ icon: 'error', title: 'No se pudo eliminar', text: msg });
}
});
});
});
// Submit del form en el modal
$(document).on('submit', '#userForm', function (e) {
e.preventDefault();
@ -59,7 +127,7 @@ $(() => {
if (typeof html === 'string' && html.indexOf('id="userForm"') !== -1 && html.indexOf('<html') === -1) {
$('#userModalBody').html(html);
const isEdit = $('#userModalBody #userForm input[name="_method"][value="PUT"]').length > 0;
const title = $('#userModalBody #userForm').data(isEdit ? 'edit' : 'add');
const title = $('#userModalBody #userForm').data(isEdit ? 'edit' : 'add');
$('#userFormModal .modal-title').text(title);
return;
}
@ -72,7 +140,7 @@ $(() => {
if (xhr.status === 422 && xhr.responseText) {
$('#userModalBody').html(xhr.responseText);
const isEdit = $('#userModalBody #userForm input[name="_method"][value="PUT"]').length > 0;
const title = $('#userModalBody #userForm').data(isEdit ? 'edit' : 'add');
const title = $('#userModalBody #userForm').data(isEdit ? 'edit' : 'add');
$('#userFormModal .modal-title').text(title);
return;
}

View File

@ -2,9 +2,13 @@
th:with="isAuth=${#authorization.expression('isAuthenticated()')}"
th:attrappend="data-layout=${isAuth} ? 'semibox' : 'horizontal'" data-sidebar-visibility="show" data-topbar="light"
data-sidebar="light" data-sidebar-size="lg" data-sidebar-image="none" data-preloader="disable"
xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="_csrf" th:content="${_csrf.token}" />
<meta name="_csrf_header" th:content="${_csrf.headerName}" />
<th:block layout:fragment="pagetitle" />
<th:block th:replace="~{imprimelibros/partials/head-css :: head-css}" />
<link href="/assets/libs/sweetalert2/sweetalert2.min.css" rel="stylesheet" type="text/css" />

View File

@ -49,6 +49,31 @@
<th scope="col" th:text="#{usuarios.tabla.estado}">Estado</th>
<th scope="col" th:text="#{usuarios.tabla.acciones}">Acciones</th>
</tr>
<tr>
<th><input type="text" class="form-control form-control-sm user-filter" data-col="id" />
</th>
<th><input type="text" class="form-control form-control-sm user-filter"
data-col="fullName" /></th>
<th><input type="text" class="form-control form-control-sm user-filter"
data-col="userName" /></th>
<th>
<select class="form-select form-select-sm user-filter-select" id="search-role">
<option value="" th:text="#{usuarios.todos}">Todos</option>
<option value="USER" th:text="#{usuarios.rol.user}">Usuario</option>
<option value="ADMIN" th:text="#{usuarios.rol.admin}">Administrador</option>
<option value="SUPERADMIN" th:text="#{usuarios.rol.superadmin}">Super Administrador
</option>
</select>
</th>
<th>
<select class="form-select form-select-sm user-filter-select" id="search-status">
<option value="" th:text="#{usuarios.todos}">Todos</option>
<option value="true" th:text="#{usuarios.tabla.activo}">Activo</option>
<option value="false" th:text="#{usuarios.tabla.inactivo}">Inactivo</option>
</select>
</th>
<th></th>
</tr>
</thead>
<tbody>
</tbody>