Files
erp-imprimelibros/src/main/java/com/imprimelibros/erp/direcciones/DireccionController.java

529 lines
22 KiB
Java

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<String> 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<String, String> 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<Map<String, Object>> 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<String> searchable = List.of(
"id",
"cliente", "alias",
"att", "direccion", "cp", "ciudad", "provincia", "pais");
List<String> orderable = List.of(
"id",
"cliente", "alias",
"att", "direccion", "cp", "ciudad", "provincia", "pais");
// Filtro base por rol (ROLE_USER solo ve sus direcciones)
Specification<Direccion> base = (root, query, cb) -> {
List<Predicate> 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 -> """
<div class="hstack gap-3 flex-wrap">
<a href="javascript:void(0);" data-id="%d" class="link-success btn-edit-direccion fs-15">
<i class="ri-edit-2-line"></i>
</a>
<a href="javascript:void(0);" data-id="%d" class="link-danger btn-delete-direccion fs-15">
<i class="ri-delete-bin-5-line"></i>
</a>
</div>
""".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<Map<String, Object>> 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<String> searchable = List.of(
"id",
"alias",
"att", "direccion", "cp", "ciudad", "provincia", "pais", "telefono", "is_facturacion", "razonSocial",
"identificacionFiscal");
List<String> orderable = List.of(
"id",
"cliente", "alias",
"att", "direccion", "cp", "ciudad", "provincia", "pais", "telefono");
// Filtro base por rol (ROLE_USER solo ve sus direcciones)
Specification<Direccion> base = (root, query, cb) -> {
List<Predicate> 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<String, Object> 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);
}
}