package com.imprimelibros.erp.direcciones; import java.security.Principal; import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; import org.springframework.context.MessageSource; import org.springframework.data.jpa.domain.Specification; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Controller; import org.springframework.transaction.annotation.Transactional; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.DeleteMapping; 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; import org.springframework.web.bind.annotation.ResponseBody; import com.imprimelibros.erp.cart.CartService; 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.User; 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") public class DireccionController { private final DireccionService direccionService; protected final DireccionRepository repo; protected final PaisesService paisesService; protected final MessageSource messageSource; protected final UserDao userRepo; protected final TranslationService translationService; protected final CartService cartService; public DireccionController(DireccionRepository repo, PaisesService paisesService, MessageSource messageSource, UserDao userRepo, TranslationService translationService, DireccionService direccionService, CartService cartService) { this.repo = repo; this.paisesService = paisesService; this.messageSource = messageSource; this.userRepo = userRepo; this.translationService = translationService; this.direccionService = direccionService; this.cartService = cartService; } @GetMapping() public String viewDirecciones(Model model, Authentication auth, Locale locale) { boolean isUser = auth != null && auth.getAuthorities().stream() .anyMatch(a -> a.getAuthority().equals("ROLE_USER")); model.addAttribute("isUser", isUser ? 1 : 0); List keys = List.of( "direcciones.delete.title", "direcciones.delete.text", "direcciones.eliminar", "direcciones.delete.button", "app.yes", "app.cancelar", "direcciones.delete.ok.title", "direcciones.delete.ok.text", "direcciones.btn.edit", "direcciones.btn.delete", "direcciones.telefono", "direcciones.isFacturacionShort"); Map translations = translationService.getTranslations(locale, keys); model.addAttribute("languageBundle", translations); if (isUser) return "imprimelibros/direcciones/direccion-list-cliente"; else return "imprimelibros/direcciones/direccion-list"; } @GetMapping(value = "/datatable", produces = "application/json") @ResponseBody public DataTablesResponse> datatable( HttpServletRequest request, Authentication authentication, Locale locale) { DataTablesRequest dt = DataTablesParser.from(request); // Columnas visibles / lógicas para el DataTable en el frontend: // id, cliente (nombre de usuario), alias, att, direccion, cp, ciudad, // provincia, pais List searchable = List.of( "id", "cliente", "alias", "att", "direccion", "cp", "ciudad", "provincia", "pais"); List orderable = List.of( "id", "cliente", "alias", "att", "direccion", "cp", "ciudad", "provincia", "pais"); // Filtro base por rol (ROLE_USER solo ve sus direcciones) Specification base = (root, query, cb) -> { List predicates = new ArrayList<>(); 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)); } return cb.and(predicates.toArray(new Predicate[0])); }; long total = repo.count(base); // Construcción del datatable con entity + spec return DataTable .of(repo, Direccion.class, dt, searchable) .orderable(orderable) // Columnas "crudas" (las que existen tal cual): .edit("id", d -> d.getId()) .edit("alias", d -> d.getAlias()) .edit("att", d -> d.getAtt()) .edit("direccion", d -> d.getDireccion()) .edit("cp", d -> d.getCp()) .edit("ciudad", d -> d.getCiudad()) .edit("provincia", d -> d.getProvincia()) // Columnas calculadas: // cliente = nombre del usuario (o username si no tienes name) .add("cliente", d -> { var u = d.getUser(); return (u != null && u.getFullName() != null && !u.getFullName().isBlank()) ? u.getFullName() : ""; }) // pais = nombre localizado desde MessageSource usando el keyword del país .add("pais", d -> { // si tienes la relación read-only a Pais (d.getPais()) con .getKeyword() String keyword = (d.getPais() != null) ? d.getPais().getKeyword() : null; if (keyword == null || keyword.isBlank()) return d.getPaisCode3(); return messageSource.getMessage("paises." + keyword, null, keyword, locale); }) // Ejemplo de columna de acciones: .add("actions", d -> """ """.formatted(d.getId(), d.getId())) // WHERE dinámico (spec base) .where(base) // Si tu DataTable helper soporta “join/alias” para buscar/ordenar por campos // relacionados: // .searchAlias("cliente", (root, cb) -> root.join("user").get("name")) // .orderAlias("cliente", (root) -> root.join("user").get("name")) // .searchAlias("pais", (root, cb) -> root.join("pais", // JoinType.LEFT).get("keyword")) // .orderAlias("pais", (root) -> root.join("pais", // JoinType.LEFT).get("keyword")) .toJson(total); } @GetMapping(value = "/datatableDirecciones", produces = "application/json") @ResponseBody public DataTablesResponse> datatableCliente( HttpServletRequest request, Authentication authentication, Locale locale) { DataTablesRequest dt = DataTablesParser.from(request); // Columnas visibles / lógicas para el DataTable en el frontend: // id, cliente (nombre de usuario), alias, att, direccion, cp, ciudad, // provincia, pais List searchable = List.of( "id", "alias", "att", "direccion", "cp", "ciudad", "provincia", "pais", "telefono", "is_facturacion", "razonSocial", "identificacionFiscal"); List orderable = List.of( "id", "cliente", "alias", "att", "direccion", "cp", "ciudad", "provincia", "pais", "telefono"); // Filtro base por rol (ROLE_USER solo ve sus direcciones) Specification base = (root, query, cb) -> { List predicates = new ArrayList<>(); 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)); } return cb.and(predicates.toArray(new Predicate[0])); }; long total = repo.count(base); // Construcción del datatable con entity + spec return DataTable .of(repo, Direccion.class, dt, searchable) .orderable(orderable) // Columnas "crudas" (las que existen tal cual): .edit("id", d -> d.getId()) .edit("alias", d -> d.getAlias()) .edit("att", d -> d.getAtt()) .edit("direccion", d -> d.getDireccion()) .edit("cp", d -> d.getCp()) .edit("ciudad", d -> d.getCiudad()) .edit("provincia", d -> d.getProvincia()) .edit("telefono", d -> d.getTelefono()) .edit("is_facturacion", d -> d.isDireccionFacturacion()) .edit("razon_social", d -> d.getRazonSocial()) .edit("tipo_identificacion_fiscal", d -> d.getTipoIdentificacionFiscal()) .edit("identificacion_fiscal", d -> d.getIdentificacionFiscal()) // pais = nombre localizado desde MessageSource usando el keyword del país .add("pais", d -> { // si tienes la relación read-only a Pais (d.getPais()) con .getKeyword() String keyword = (d.getPais() != null) ? d.getPais().getKeyword() : null; if (keyword == null || keyword.isBlank()) return d.getPaisCode3(); return messageSource.getMessage("paises." + keyword, null, keyword, locale); }) // WHERE dinámico (spec base) .where(base) .toJson(total); } @GetMapping("form") public String getForm(@RequestParam(required = false) Long id, Direccion direccion, BindingResult binding, Model model, HttpServletResponse response, Authentication auth, Locale locale) { model.addAttribute("paises", paisesService.getForSelect("", "", locale).get("results")); if (id != null) { var opt = repo.findByIdWithPaisAndUser(id); if (opt == null) { binding.reject("direcciones.error.noEncontrado", messageSource.getMessage("direcciones.error.noEncontrado", null, locale)); response.setStatus(404); model.addAttribute("action", "/direcciones/" + id); return "imprimelibros/direcciones/direccion-form :: direccionForm"; } model.addAttribute("dirForm", opt.get()); model.addAttribute("action", "/direcciones/" + id); } else { Direccion newDireccion = new Direccion(); boolean isUser = auth != null && auth.getAuthorities().stream() .anyMatch(a -> a.getAuthority().equals("ROLE_USER")); if (isUser) { User user = direccion.getUser() != null ? direccion.getUser() : null; if (user != null) { newDireccion.setUser(user); } } model.addAttribute("dirForm", newDireccion); model.addAttribute("action", "/direcciones"); } return "imprimelibros/direcciones/direccion-form :: direccionForm"; } @GetMapping("direction-form") public String getForm(@RequestParam(required = false) Long id, Direccion direccion, BindingResult binding, Model model, HttpServletResponse response, Principal principal, Locale locale) { model.addAttribute("paises", paisesService.getForSelect("", "", locale).get("results")); Direccion newDireccion = new Direccion(); User user = null; if (principal instanceof UserDetailsImpl udi) { user = new User(); user.setId(udi.getId()); } else if (principal instanceof User u && u.getId() != null) { user = u; } newDireccion.setUser(user); model.addAttribute("dirForm", newDireccion); model.addAttribute("action", "/direcciones/add"); return "imprimelibros/direcciones/direccion-form-fixed-user :: direccionForm"; } @PostMapping public String create( @Valid @ModelAttribute("dirForm") Direccion direccion, BindingResult binding, Model model, HttpServletResponse response, Authentication auth, Locale locale) { boolean isUser = auth != null && auth.getAuthorities().stream() .anyMatch(a -> a.getAuthority().equals("ROLE_USER")); if (isUser) { User current = userRepo.findByUserNameIgnoreCaseAndEnabledTrueAndDeletedFalse(auth.getName()).orElse(null); direccion.setUser(current); // ignora lo que venga del hidden } if (binding.hasErrors()) { response.setStatus(422); 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; repo.save(data); response.setStatus(201); return null; } // para el formulario modal en checkout @PostMapping("/add") public String create2( @Valid @ModelAttribute("dirForm") Direccion direccion, BindingResult binding, Model model, HttpServletResponse response, Authentication auth, Locale locale) { User current = userRepo.findByUserNameIgnoreCaseAndEnabledTrueAndDeletedFalse(auth.getName()).orElse(null); direccion.setUser(current); if (binding.hasErrors()) { response.setStatus(422); model.addAttribute("paises", paisesService.getForSelect("", "", locale).get("results")); model.addAttribute("action", "/direcciones/add"); model.addAttribute("dirForm", direccion); return "imprimelibros/direcciones/direccion-form-fixed-user :: direccionForm"; } var data = direccion; repo.save(data); response.setStatus(201); return null; } @PostMapping("/{id}") public String update( @PathVariable Long id, @Valid @ModelAttribute("dirForm") Direccion direccion, // <- nombre distinto BindingResult binding, Model model, Authentication auth, HttpServletResponse response, Locale 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("dirForm", direccion); // <- importante model.addAttribute("paises", paisesService.getForSelect("", "", locale).get("results")); model.addAttribute("action", "/direcciones/" + id); return "imprimelibros/direcciones/direccion-form :: direccionForm"; } repo.save(direccion); response.setStatus(200); return null; } @DeleteMapping("/{id}") @Transactional public ResponseEntity delete(@PathVariable Long id, Authentication auth, Locale locale) { Direccion direccion = repo.findById(id).orElse(null); if (direccion == null) { return ResponseEntity.status(HttpStatus.NOT_FOUND) .body(Map.of("message", messageSource.getMessage("direcciones.error.noEncontrado", null, locale))); } boolean isUser = auth != null && auth.getAuthorities().stream() .anyMatch(a -> a.getAuthority().equals("ROLE_USER")); Long ownerId = direccion.getUser() != null ? direccion.getUser().getId() : null; Boolean isOwner = this.isOwnerOrAdmin(auth, ownerId); if (isUser && !isOwner) { return ResponseEntity.status(HttpStatus.FORBIDDEN) .body(Map.of("message", messageSource.getMessage("direcciones.error.sinPermiso", null, locale))); } try { direccion.setDeleted(true); direccion.setDeletedAt(Instant.now()); if (auth != null && auth.getPrincipal() instanceof UserDetailsImpl udi) { direccion.setDeletedBy(userRepo.getReferenceById(udi.getId())); } else if (auth != null) { userRepo.findByUserNameIgnoreCase(auth.getName()).ifPresent(direccion::setDeletedBy); } repo.saveAndFlush(direccion); // eliminar referencias en carritos activos cartService.deleteCartDireccionesByDireccionId(direccion.getId()); return ResponseEntity.ok(Map.of("message", messageSource.getMessage("direcciones.exito.eliminado", null, locale))); } catch (Exception ex) { // Devuelve SIEMPRE algo en el catch return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(Map.of("message", messageSource.getMessage("direcciones.error.delete-internal-error", null, locale), "detail", ex.getClass().getSimpleName() + ": " + (ex.getMessage() != null ? ex.getMessage() : ""))); } } @GetMapping(value = "/select2", produces = "application/json") @ResponseBody public Map getSelect2( @RequestParam(value = "q", required = false) String q1, @RequestParam(value = "term", required = false) String q2, @RequestParam(value = "presupuestoId", required = false) Long presupuestoId, Authentication auth) { boolean isAdmin = auth.getAuthorities().stream() .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN") || a.getAuthority().equals("ROLE_SUPERADMIN")); Long currentUserId = null; if (!isAdmin) { if (auth != null && auth.getPrincipal() instanceof UserDetailsImpl udi) { currentUserId = udi.getId(); } else if (auth != null) { currentUserId = userRepo.findIdByUserNameIgnoreCase(auth.getName()).orElse(null); } } return direccionService.getForSelect(q1, q2, isAdmin ? null : currentUserId); } private boolean isOwnerOrAdmin(Authentication auth, Long ownerId) { if (auth == null) { return false; } boolean isAdmin = auth.getAuthorities().stream() .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN") || a.getAuthority().equals("ROLE_SUPERADMIN")); 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); } }