falta borrar direcciones e implementar una vista de cliente

This commit is contained in:
2025-10-25 15:18:17 +02:00
parent 2ed032d7c6
commit 8e011e7fca
10 changed files with 292 additions and 226 deletions

View File

@ -1,5 +1,6 @@
package com.imprimelibros.erp.config;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
@ -10,13 +11,16 @@ import jakarta.validation.ValidatorFactory;
@Configuration
public class BeanValidationConfig {
// Asegura que usamos la factory de Spring (con SpringConstraintValidatorFactory)
// Usa el MessageSource (messages*.properties) para resolver {códigos}
@Bean
public LocalValidatorFactoryBean validator() {
return new LocalValidatorFactoryBean();
public LocalValidatorFactoryBean validator(MessageSource messageSource) {
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setValidationMessageSource(messageSource); // <-- CLAVE
return bean;
}
// Inserta esa factory en Hibernate/JPA
// Inserta esa factory en Hibernate/JPA (opcional pero correcto)
@Bean
public HibernatePropertiesCustomizer hibernateValidationCustomizer(ValidatorFactory vf) {
return props -> props.put("jakarta.persistence.validation.factory", vf);

View File

@ -10,6 +10,9 @@ import com.imprimelibros.erp.common.jpa.AbstractAuditedEntity;
import com.imprimelibros.erp.paises.Paises;
import com.imprimelibros.erp.users.User;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.NotBlank;
@Entity
@Table(name = "direcciones", indexes = {
@Index(name = "idx_direcciones_user", columnList = "user_id"),
@ -42,32 +45,41 @@ public class Direccion extends AbstractAuditedEntity implements Serializable {
private Long id;
// --- FK a users(id)
@NotNull(message = "{direcciones.form.error.required}")
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@NotBlank(message = "{direcciones.form.error.required}")
@Column(name = "alias", length = 100, nullable = false)
private String alias;
@NotBlank(message = "{direcciones.form.error.required}")
@Column(name = "att", length = 150, nullable = false)
private String att;
@NotBlank(message = "{direcciones.form.error.required}")
@Column(name = "direccion", length = 255, nullable = false)
private String direccion;
@NotNull(message = "{direcciones.form.error.required}")
@Column(name = "cp", length = 20, nullable = false)
private Integer cp;
@NotBlank(message = "{direcciones.form.error.required}")
@Column(name = "ciudad", length = 100, nullable = false)
private String ciudad;
@NotBlank(message = "{direcciones.form.error.required}")
@Column(name = "provincia", length = 100, nullable = false)
private String provincia;
// Usamos el code3 del país como FK lógica (String)
@NotBlank(message = "{direcciones.form.error.required}")
@Column(name = "pais_code3", length = 3, nullable = false)
private String paisCode3 = "esp";
@NotBlank(message = "{direcciones.form.error.required}")
@Column(name = "telefono", length = 30, nullable = false)
private String telefono;

View File

@ -12,6 +12,8 @@ import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@ -21,11 +23,15 @@ import com.imprimelibros.erp.datatables.DataTable;
import com.imprimelibros.erp.datatables.DataTablesParser;
import com.imprimelibros.erp.datatables.DataTablesRequest;
import com.imprimelibros.erp.datatables.DataTablesResponse;
import com.imprimelibros.erp.i18n.TranslationService;
import com.imprimelibros.erp.paises.PaisesService;
import com.imprimelibros.erp.users.UserDao;
import com.imprimelibros.erp.users.UserDetailsImpl;
import jakarta.persistence.criteria.Predicate;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
@Controller
@RequestMapping("/direcciones")
@ -34,11 +40,16 @@ public class DireccionController {
protected final DireccionRepository repo;
protected final PaisesService paisesService;
protected final MessageSource messageSource;
protected final UserDao userRepo;
protected final TranslationService translationService;
public DireccionController(DireccionRepository repo, PaisesService paisesService, MessageSource messageSource) {
public DireccionController(DireccionRepository repo, PaisesService paisesService,
MessageSource messageSource, UserDao userRepo, TranslationService translationService) {
this.repo = repo;
this.paisesService = paisesService;
this.messageSource = messageSource;
this.userRepo = userRepo;
this.translationService = translationService;
}
@GetMapping()
@ -48,6 +59,19 @@ public class DireccionController {
.anyMatch(a -> a.getAuthority().equals("ROLE_USER"));
model.addAttribute("isUser", isUser ? 1 : 0);
List<String> keys = List.of(
"margenes-presupuesto.delete.title",
"margenes-presupuesto.delete.text",
"margenes-presupuesto.eliminar",
"margenes-presupuesto.delete.button",
"app.yes",
"app.cancelar",
"margenes-presupuesto.delete.ok.title",
"margenes-presupuesto.delete.ok.text");
Map<String, String> translations = translationService.getTranslations(locale, keys);
model.addAttribute("languageBundle", translations);
return "imprimelibros/direcciones/direccion-list";
}
@ -80,13 +104,13 @@ public class DireccionController {
if (authentication != null && authentication.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_USER"))) {
String username = authentication.getName();
predicates.add(cb.equal(root.get("user").get("username"), username));
predicates.add(cb.equal(root.get("user").get("userName"), username));
}
return cb.and(predicates.toArray(new Predicate[0]));
};
long total = repo.count(); // total sin filtro global
long total = repo.count(base);
// Construcción del datatable con entity + spec
return DataTable
@ -168,11 +192,11 @@ public class DireccionController {
return "imprimelibros/direcciones/direccion-form :: direccionForm";
}
model.addAttribute("direccion", opt.get());
model.addAttribute("dirForm", opt.get());
model.addAttribute("action", "/direcciones/" + id);
} else {
model.addAttribute("direccion", new Direccion());
model.addAttribute("dirForm", new Direccion());
model.addAttribute("action", "/direcciones");
}
return "imprimelibros/direcciones/direccion-form :: direccionForm";
@ -180,7 +204,7 @@ public class DireccionController {
@PostMapping
public String create(
Direccion direccion,
@Valid @ModelAttribute("dirForm") Direccion direccion,
BindingResult binding,
Model model,
HttpServletResponse response,
@ -188,130 +212,110 @@ public class DireccionController {
if (binding.hasErrors()) {
response.setStatus(422);
model.addAttribute("action", "/direcciones/");
model.addAttribute("paises", paisesService.getForSelect("", "", locale).get("results"));
model.addAttribute("action", "/direcciones");
model.addAttribute("dirForm", direccion);
return "imprimelibros/direcciones/direccion-form :: direccionForm";
}
var data = direccion;
try {
repo.save(data);
} catch (jakarta.validation.ConstraintViolationException vex) {
// Errores de Bean Validation disparados al flush (incluye tu @NoRangeOverlap)
vex.getConstraintViolations().forEach(v -> {
// intenta asignar al campo si existe, si no, error global
String path = v.getPropertyPath() != null ? v.getPropertyPath().toString() : null;
String code = v.getMessage() != null ? v.getMessage().trim() : "";
if (code.startsWith("{") && code.endsWith("}")) {
code = code.substring(1, code.length() - 1); // -> "validation.required"
}
if (path != null && binding.getFieldError(path) == null) {
binding.rejectValue(path, "validation", messageSource.getMessage(code, null, locale));
} else {
binding.reject("validation", messageSource.getMessage(code, null, locale));
}
});
response.setStatus(422);
model.addAttribute("action", "/direcciones/");
return "imprimelibros/direcciones/direccion-form :: direccionForm";
}
repo.save(data);
response.setStatus(201);
return null;
}
/*
@PutMapping("/{id}")
public String edit(
@PostMapping("/{id}")
public String update(
@PathVariable Long id,
MargenPresupuesto form,
@Valid @ModelAttribute("dirForm") Direccion direccion, // <- nombre distinto
BindingResult binding,
Model model,
Authentication auth,
HttpServletResponse response,
Locale locale) {
var uOpt = repo.findById(id);
if (uOpt.isEmpty()) {
binding.reject("usuarios.error.noEncontrado",
messageSource.getMessage("usuarios.error.noEncontrado", null, locale));
var opt = repo.findById(id);
if (opt.isEmpty()) {
binding.reject("direcciones.error.noEncontrado",
messageSource.getMessage("direcciones.error.noEncontrado", null, locale));
response.setStatus(404);
model.addAttribute("dirForm", direccion); // por si re-renderiza
model.addAttribute("action", "/direcciones/" + id);
return "imprimelibros/direcciones/direccion-form :: direccionForm";
}
Long ownerId = opt.get().getUser() != null ? opt.get().getUser().getId() : null;
if (!isOwnerOrAdmin(auth, ownerId)) {
binding.reject("direcciones.error.sinPermiso",
messageSource.getMessage("direcciones.error.sinPermiso", null, locale));
response.setStatus(403);
model.addAttribute("dirForm", direccion); // por si re-renderiza
model.addAttribute("action", "/direcciones/" + id);
return "imprimelibros/direcciones/direccion-form :: direccionForm";
}
if (binding.hasErrors()) {
response.setStatus(422);
model.addAttribute("action", "/configuracion/margenes-presupuesto/" + id);
return "imprimelibros/configuracion/margenes-presupuesto/margenes-presupuesto-form :: margenesPresupuestoForm";
model.addAttribute("dirForm", direccion); // <- importante
model.addAttribute("paises", paisesService.getForSelect("", "", locale).get("results"));
model.addAttribute("action", "/direcciones/" + id);
return "imprimelibros/direcciones/direccion-form :: direccionForm";
}
var entity = uOpt.get();
// 3) Copiar solamente campos editables
entity.setImporteMin(form.getImporteMin());
entity.setImporteMax(form.getImporteMax());
entity.setMargenMax(form.getMargenMax());
entity.setMargenMin(form.getMargenMin());
try {
repo.saveAndFlush(entity);
} catch (jakarta.validation.ConstraintViolationException vex) {
// Errores de Bean Validation disparados al flush (incluye tu @NoRangeOverlap)
vex.getConstraintViolations().forEach(v -> {
// intenta asignar al campo si existe, si no, error global
String path = v.getPropertyPath() != null ? v.getPropertyPath().toString() : null;
String code = v.getMessage() != null ? v.getMessage().trim() : "";
if (code.startsWith("{") && code.endsWith("}")) {
code = code.substring(1, code.length() - 1); // -> "validation.required"
}
if (path != null && binding.getFieldError(path) == null) {
binding.rejectValue(path, "validation", messageSource.getMessage(code, null, locale));
} else {
binding.reject("validation", messageSource.getMessage(code, null, locale));
}
});
response.setStatus(422);
model.addAttribute("action", "/configuracion/margenes-presupuesto/" + id);
return "imprimelibros/configuracion/margenes-presupuesto/margenes-presupuesto-form :: margenesPresupuestoForm";
} catch (org.springframework.dao.DataIntegrityViolationException dex) {
// Uniques, FKs, checks… mensajes de la BD
String msg = dex.getMostSpecificCause() != null ? dex.getMostSpecificCause().getMessage()
: dex.getMessage();
binding.reject("db.error", messageSource.getMessage(msg, null, locale));
response.setStatus(422);
model.addAttribute("action", "/configuracion/margenes-presupuesto/" + id);
return "imprimelibros/configuracion/margenes-presupuesto/margenes-presupuesto-form :: margenesPresupuestoForm";
}
response.setStatus(204);
repo.save(direccion);
response.setStatus(200);
return null;
}
@DeleteMapping("/{id}")
@Transactional
public ResponseEntity<?> delete(@PathVariable Long id, Authentication auth, Locale locale) {
/*
*
* @DeleteMapping("/{id}")
*
* @Transactional
* public ResponseEntity<?> delete(@PathVariable Long id, Authentication auth,
* Locale locale) {
*
* return repo.findById(id).map(u -> {
* try {
* u.setDeleted(true);
* u.setDeletedAt(LocalDateTime.now());
*
* repo.save(u); // ← NO delete(); guardamos el soft delete con deleted_by
* relleno
* return ResponseEntity.ok(Map.of("message",
* messageSource.getMessage("margenes-presupuesto.exito.eliminado", null,
* locale)));
* } catch (Exception ex) {
* return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
* .body(Map.of("message",
* messageSource.getMessage("margenes-presupuesto.error.delete-internal-error",
* null,
* locale)));
* }
* }).orElseGet(() -> ResponseEntity.status(HttpStatus.NOT_FOUND)
* .body(Map.of("message",
* messageSource.getMessage("margenes-presupuesto.error.not-found", null,
* locale))));
* }
*/
return repo.findById(id).map(u -> {
try {
u.setDeleted(true);
u.setDeletedAt(LocalDateTime.now());
repo.save(u); // ← NO delete(); guardamos el soft delete con deleted_by relleno
return ResponseEntity.ok(Map.of("message",
messageSource.getMessage("margenes-presupuesto.exito.eliminado", null, locale)));
} catch (Exception ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("message",
messageSource.getMessage("margenes-presupuesto.error.delete-internal-error", null,
locale)));
}
}).orElseGet(() -> ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(Map.of("message",
messageSource.getMessage("margenes-presupuesto.error.not-found", null, locale))));
private boolean isOwnerOrAdmin(Authentication auth, Long ownerId) {
if (auth == null) {
return false;
}
boolean isAdmin = auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
if (isAdmin) {
return true;
}
// Aquí deberías obtener el ID del usuario actual desde tu servicio de usuarios
Long currentUserId = null;
if (auth != null && auth.getPrincipal() instanceof UserDetailsImpl udi) {
currentUserId = udi.getId();
} else if (auth != null) {
currentUserId = userRepo.findIdByUserNameIgnoreCase(auth.getName()).orElse(null);
}
return currentUserId != null && currentUserId.equals(ownerId);
}
*/
}

View File

@ -13,54 +13,22 @@ public interface DireccionRepository
extends JpaRepository<Direccion, Long>,
JpaSpecificationExecutor<Direccion> {
@Query("""
SELECT
d.id AS id,
d.alias AS alias,
d.att AS att,
d.direccion AS direccion,
d.cp AS cp,
d.ciudad AS ciudad,
d.provincia AS provincia,
d.paisCode3 AS paisCode3,
p.keyword AS paisKeyword,
d.telefono AS telefono,
d.direccionFacturacion AS direccionFacturacion,
d.razonSocial AS razonSocial,
d.tipoIdentificacionFiscal AS tipoIdentificacionFiscal,
d.identificacionFiscal AS identificacionFiscal,
u.fullName AS cliente
FROM Direccion d
JOIN d.user u
LEFT JOIN Paises p ON d.paisCode3 = p.code3
WHERE (:userId IS NULL OR u.id = :userId)
""")
select d from Direccion d
left join fetch d.user
left join fetch d.pais
where d.user.id = :id
""")
List<DireccionView> findAllWithPaisAndUser(@Param("userId") Long userId);
//findbyidwithPaisAndUser
@Query("""
SELECT
d.id AS id,
d.alias AS alias,
d.att AS att,
d.direccion AS direccion,
d.cp AS cp,
d.ciudad AS ciudad,
d.provincia AS provincia,
d.paisCode3 AS paisCode3,
p.keyword AS paisKeyword,
d.telefono AS telefono,
d.direccionFacturacion AS direccionFacturacion,
d.razonSocial AS razonSocial,
d.tipoIdentificacionFiscal AS tipoIdentificacionFiscal,
d.identificacionFiscal AS identificacionFiscal,
u.fullName AS cliente
FROM Direccion d
JOIN d.user u
LEFT JOIN Paises p ON d.paisCode3 = p.code3
WHERE (d.id = :id)
""")
Optional<DireccionView> findByIdWithPaisAndUser(@Param("id") Long id);
select d from Direccion d
left join fetch d.user
left join fetch d.pais
where d.id = :id
""")
Optional<Direccion> findByIdWithPaisAndUser(@Param("id") Long id);

View File

@ -2,13 +2,14 @@ package com.imprimelibros.erp.direcciones;
public interface DireccionView {
Long getId();
UserView getUser();
String getAlias();
String getAtt();
String getDireccion();
String getCp();
String getCiudad();
String getProvincia();
String getPaisCode3();
PaisView getPais();
String getPaisKeyword();
String getTelefono();
Boolean getIsFacturacion();
@ -16,4 +17,12 @@ public interface DireccionView {
String getTipoIdentificacionFiscal();
String getIdentificacionFiscal();
String getCliente();
interface UserView {
Long getId();
String getFullName();
}
interface PaisView {
String getCode3();
String getKeyword();
}
}