package com.imprimelibros.erp.presupuesto; import org.springframework.stereotype.Controller; import org.springframework.transaction.annotation.Transactional; import org.springframework.ui.Model; import java.security.Principal; import java.time.Instant; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.List; import java.util.Set; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.servlet.mvc.support.RedirectAttributes; 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.RequestBody; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import jakarta.validation.Validator; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.core.type.TypeReference; import com.imprimelibros.erp.configurationERP.VariableService; import com.imprimelibros.erp.datatables.*; import com.imprimelibros.erp.externalApi.skApiClient; import com.imprimelibros.erp.i18n.TranslationService; import com.imprimelibros.erp.presupuesto.classes.ImagenPresupuesto; import com.imprimelibros.erp.presupuesto.classes.PresupuestoMaquetacion; import com.imprimelibros.erp.presupuesto.classes.PresupuestoMarcapaginas; import com.imprimelibros.erp.presupuesto.dto.Presupuesto; import com.imprimelibros.erp.presupuesto.service.PresupuestoService; import com.imprimelibros.erp.presupuesto.validation.PresupuestoValidationGroups; import com.imprimelibros.erp.users.UserDao; import com.imprimelibros.erp.users.UserDetailsImpl; import com.imprimelibros.erp.presupuesto.service.PresupuestoFormDataMapper; import com.imprimelibros.erp.presupuesto.service.PresupuestoFormDataMapper.PresupuestoFormDataDto; import com.imprimelibros.erp.common.Utils; import com.imprimelibros.erp.common.web.IpUtils; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.ConstraintViolation; import jakarta.validation.Valid; @Controller @RequestMapping("/presupuesto") public class PresupuestoController { private final PresupuestoRepository presupuestoRepository; @Autowired protected PresupuestoService presupuestoService; @Autowired protected skApiClient apiClient; @Autowired protected MessageSource messageSource; @Autowired private Validator validator; private final ObjectMapper objectMapper; private final TranslationService translationService; private final PresupuestoDatatableService dtService; private final VariableService variableService; private final PresupuestoFormDataMapper formDataMapper; private final UserDao userRepo; public PresupuestoController(ObjectMapper objectMapper, TranslationService translationService, PresupuestoDatatableService dtService, PresupuestoRepository presupuestoRepository, VariableService variableService, PresupuestoFormDataMapper formDataMapper, UserDao userRepo) { this.objectMapper = objectMapper; this.translationService = translationService; this.dtService = dtService; this.presupuestoRepository = presupuestoRepository; this.variableService = variableService; this.formDataMapper = formDataMapper; this.userRepo = userRepo; } @PostMapping("/public/validar/datos-generales") public ResponseEntity validarDatosGenerales( @Validated(PresupuestoValidationGroups.DatosGenerales.class) Presupuesto presupuesto, BindingResult result, Locale locale) { Map errores = new HashMap<>(); result.getFieldErrors().forEach(error -> { String code = Objects.requireNonNullElse(error.getDefaultMessage(), "").replace("{", "").replace("}", ""); String msg = messageSource.getMessage(code, null, locale); errores.put(error.getField(), msg); }); result.getGlobalErrors().forEach(error -> { errores.put("global", error.getDefaultMessage()); }); if (!errores.isEmpty()) { return ResponseEntity.badRequest().body(errores); } // opciones color Map resultado = presupuestoService.obtenerOpcionesColor(presupuesto, locale); // opciones papel interior resultado.putAll(presupuestoService.obtenerOpcionesPapelInterior(presupuesto, locale)); // opciones gramaje interior resultado.putAll(presupuestoService.obtenerOpcionesGramajeInterior(presupuesto)); return ResponseEntity.ok(resultado); } @PostMapping("/public/validar/interior") public ResponseEntity validarInterior( @Validated(PresupuestoValidationGroups.Interior.class) Presupuesto presupuesto, BindingResult result, Locale locale) { Map errores = new HashMap<>(); // errores de campos individuales result.getFieldErrors().forEach(error -> { String code = Objects.requireNonNullElse(error.getDefaultMessage(), "").replace("{", "").replace("}", ""); String msg = messageSource.getMessage(code, null, locale); errores.put(error.getField(), msg); }); // errores globales (@ConsistentTiradas...) result.getGlobalErrors().forEach(error -> errores.put("global", error.getDefaultMessage())); if (!errores.isEmpty()) { return ResponseEntity.badRequest().body(errores); } Map resultado = new HashMap<>(); resultado.put("solapas", apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto), locale)); resultado.putAll(presupuestoService.obtenerOpcionesAcabadosCubierta(presupuesto, locale)); return ResponseEntity.ok(resultado); } @PostMapping("/public/validar/cubierta") public ResponseEntity validarCubierta( @Validated(PresupuestoValidationGroups.Cubierta.class) Presupuesto presupuesto, BindingResult result, @RequestParam(name = "calcular", defaultValue = "true") boolean calcular, Locale locale) { Map errores = new HashMap<>(); // errores de campos individuales result.getFieldErrors().forEach(error -> { String code = Objects.requireNonNullElse(error.getDefaultMessage(), "").replace("{", "").replace("}", ""); String msg = messageSource.getMessage(code, null, locale); errores.put(error.getField(), msg); }); // errores globales (@ConsistentTiradas...) result.getGlobalErrors().forEach(error -> errores.put("global", error.getDefaultMessage())); if (!errores.isEmpty()) { return ResponseEntity.badRequest().body(errores); } if (calcular) { HashMap price = presupuestoService.calcularPresupuesto(presupuesto, locale); if (!price.containsKey("data")) { return ResponseEntity.badRequest() .body(messageSource.getMessage("presupuesto.error-obtener-precio", null, locale)); } return ResponseEntity.ok(price.get("data")); } return ResponseEntity.ok().build(); } @PostMapping("/public/validar/seleccion-tirada") public ResponseEntity validarSeleccionTirada( Presupuesto presupuesto, BindingResult result, Locale locale) { Map errores = new HashMap<>(); // errores de campos individuales result.getFieldErrors().forEach(error -> { String code = Objects.requireNonNullElse(error.getDefaultMessage(), "").replace("{", "").replace("}", ""); String msg = messageSource.getMessage(code, null, locale); errores.put(error.getField(), msg); }); // errores globales (@ConsistentTiradas...) result.getGlobalErrors().forEach(error -> errores.put("global", error.getDefaultMessage())); if (!errores.isEmpty()) { return ResponseEntity.badRequest().body(errores); } Map resultado = new HashMap<>(); // servicios extra resultado.putAll(presupuestoService.obtenerServiciosExtras(presupuesto, locale)); Map language = new HashMap<>(); language.put("calcular", messageSource.getMessage("presupuesto.calcular", null, locale)); resultado.put("language", language); return ResponseEntity.ok(resultado); } @PostMapping("/public/get-papel-interior") public ResponseEntity getPapelInterior( @Validated(PresupuestoValidationGroups.Interior.class) Presupuesto presupuesto, BindingResult result, Locale locale) { Map errores = new HashMap<>(); // errores de campos individuales result.getFieldErrors().forEach(error -> { String code = Objects.requireNonNullElse(error.getDefaultMessage(), "").replace("{", "").replace("}", ""); String msg = messageSource.getMessage(code, null, locale); errores.put(error.getField(), msg); }); if (!errores.isEmpty()) { return ResponseEntity.badRequest().body(errores); } // opciones color Map resultado = presupuestoService.obtenerOpcionesPapelInterior(presupuesto, locale); // opciones gramaje interior resultado.putAll(presupuestoService.obtenerOpcionesGramajeInterior(presupuesto)); List opciones_papel = new ObjectMapper().convertValue( presupuestoService .obtenerOpcionesPapelInterior(presupuesto, locale) .get("opciones_papel_interior"), new TypeReference>() { }); if (opciones_papel != null && opciones_papel.stream().noneMatch( o -> o.getExtra_data().get("sk-id").equals(String.valueOf(presupuesto.getPapelInteriorId())))) { presupuesto.setPapelInteriorId(Integer.valueOf(opciones_papel.get(0).getExtra_data().get("sk-id"))); } List opciones = new ObjectMapper().convertValue(resultado.get("opciones_gramaje_interior"), new TypeReference>() { }); if (opciones != null && !opciones.isEmpty()) { String gramajeActual = presupuesto.getGramajeInterior().toString(); if (!opciones.contains(gramajeActual)) { presupuesto.setGramajeInterior(Integer.parseInt(opciones.get(0))); // Asignar primera opción } } resultado.put("solapas", apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto), locale)); return ResponseEntity.ok(resultado); } @PostMapping("/public/get-gramaje-interior") public ResponseEntity getGramajeInterior( @Validated(PresupuestoValidationGroups.Interior.class) Presupuesto presupuesto, BindingResult result, Locale locale) { Map errores = new HashMap<>(); // errores de campos individuales result.getFieldErrors().forEach(error -> { String code = Objects.requireNonNullElse(error.getDefaultMessage(), "").replace("{", "").replace("}", ""); String msg = messageSource.getMessage(code, null, locale); errores.put(error.getField(), msg); }); if (!errores.isEmpty()) { return ResponseEntity.badRequest().body(errores); } Map resultado = presupuestoService.obtenerOpcionesGramajeInterior(presupuesto); List opciones = new ObjectMapper().convertValue(resultado.get("opciones_gramaje_interior"), new TypeReference>() { }); if (opciones != null && !opciones.isEmpty()) { String gramajeActual = presupuesto.getGramajeInterior().toString(); if (!opciones.contains(gramajeActual)) { presupuesto.setGramajeInterior(Integer.parseInt(opciones.get(0))); // Asignar primera opción } } resultado.put("solapas", apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto), locale)); return ResponseEntity.ok(resultado); } @PostMapping("/public/get-max-solapas") public ResponseEntity getMaxSolapas( @Validated(PresupuestoValidationGroups.Interior.class) Presupuesto presupuesto, BindingResult result, Locale locale) { Map errores = new HashMap<>(); // errores de campos individuales result.getFieldErrors().forEach(error -> { String code = Objects.requireNonNullElse(error.getDefaultMessage(), "").replace("{", "").replace("}", ""); String msg = messageSource.getMessage(code, null, locale); errores.put(error.getField(), msg); }); if (!errores.isEmpty()) { return ResponseEntity.badRequest().body(errores); } Map resultado = new HashMap<>(); resultado.put("solapas", apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto), locale)); return ResponseEntity.ok(resultado); } @PostMapping("/public/get-papel-cubierta") public ResponseEntity getPapelCubierta( Presupuesto presupuesto, BindingResult result, Locale locale) { Map resultado = new HashMap<>(); Map papelesCubierta = presupuestoService.obtenerOpcionesPapelCubierta(presupuesto, locale); List opciones = new ObjectMapper().convertValue( presupuestoService .obtenerOpcionesPapelCubierta(presupuesto, locale) .get("opciones_papel_cubierta"), new TypeReference>() { }); if (opciones != null && opciones.stream().noneMatch( o -> o.getExtra_data().get("sk-id").equals(String.valueOf(presupuesto.getPapelCubiertaId())))) { presupuesto.setPapelCubiertaId(Integer.valueOf(opciones.get(0).getExtra_data().get("sk-id"))); } resultado.putAll(papelesCubierta); resultado.putAll(presupuestoService.obtenerOpcionesGramajeCubierta(presupuesto)); List gramajesCubierta = new ObjectMapper().convertValue( resultado.get("opciones_gramaje_cubierta"), new TypeReference>() { }); if (gramajesCubierta != null && !gramajesCubierta.isEmpty()) { String gramajeActual = presupuesto.getGramajeCubierta().toString(); if (!gramajesCubierta.contains(gramajeActual)) { presupuesto.setGramajeCubierta(Integer.parseInt(gramajesCubierta.get(0))); // Asignar primera opción } } return ResponseEntity.ok(resultado); } @PostMapping("/public/get-gramaje-cubierta") public ResponseEntity getGramajeCubierta( Presupuesto presupuesto, BindingResult result) { Map resultado = presupuestoService.obtenerOpcionesGramajeCubierta(presupuesto); List gramajesCubierta = (List) resultado.get("opciones_gramaje_cubierta"); if (gramajesCubierta != null && !gramajesCubierta.isEmpty()) { String gramajeActual = presupuesto.getGramajeCubierta().toString(); if (!gramajesCubierta.contains(gramajeActual)) { presupuesto.setGramajeCubierta(Integer.parseInt(gramajesCubierta.get(0))); // Asignar primera opción } } return ResponseEntity.ok(resultado); } @PostMapping("/public/get-acabados-cubierta") public ResponseEntity getAcabadosCubierta( Presupuesto presupuesto, BindingResult result, Locale locale) { Map resultado = presupuestoService.obtenerOpcionesAcabadosCubierta(presupuesto, locale); return ResponseEntity.ok(resultado); } @PostMapping("/public/get-price") public ResponseEntity getPrice( Presupuesto presupuesto, BindingResult result, Locale locale) { Map errores = new HashMap<>(); // errores de campos individuales result.getFieldErrors().forEach(error -> errores.put(error.getField(), error.getDefaultMessage())); // errores globales (@ConsistentTiradas...) result.getGlobalErrors().forEach(error -> errores.put("global", error.getDefaultMessage())); if (!errores.isEmpty()) { return ResponseEntity.badRequest().body(errores); } String price = apiClient.getPrice(presupuestoService.toSkApiRequest(presupuesto), presupuesto.getTipoEncuadernacion(), presupuesto.getTipoCubierta()); if (price == null || price.isEmpty()) { return ResponseEntity.badRequest().body("No se pudo obtener el precio. Intente nuevamente."); } return ResponseEntity.ok(price); } @GetMapping(value = "/public/maquetacion/form", produces = MediaType.TEXT_HTML_VALUE) public String getMaquetacionForm(Model model) { model.addAttribute("presupuestoMaquetacion", new PresupuestoMaquetacion()); return "imprimelibros/presupuestos/presupuesto-maquetacion-form :: maquetacionForm"; } @GetMapping("/public/maquetacion") public ResponseEntity getPresupuestoMaquetacion( @Valid @ModelAttribute PresupuestoMaquetacion presupuestoMaquetacion, BindingResult result, Locale locale) { if (result.hasErrors()) { // Construimos un mapa field -> mensaje para tu AJAX Map errores = result.getFieldErrors().stream() .collect(java.util.stream.Collectors.toMap( fe -> fe.getField(), fe -> fe.getDefaultMessage(), (a, b) -> a)); return ResponseEntity.badRequest().body(errores); } Map resultado = presupuestoService.getPrecioMaquetacion(presupuestoMaquetacion, locale); if ((Double) resultado.get("precio") == 0.0 && (Integer) resultado.get("numPaginasEstimadas") == 0 && (Double) resultado.get("precioPaginaEstimado") == 0.0) { return ResponseEntity.badRequest() .body(messageSource.getMessage("presupuesto.errores.presupuesto-maquetacion", null, locale)); } return ResponseEntity.ok(resultado); } @GetMapping(value = "/public/marcapaginas/form", produces = MediaType.TEXT_HTML_VALUE) public String getMarcapaginasForm(Model model) { model.addAttribute("presupuestoMarcapaginas", new PresupuestoMarcapaginas()); return "imprimelibros/presupuestos/presupuesto-marcapaginas-form :: marcapaginasForm"; } @GetMapping("/public/marcapaginas") public ResponseEntity getPresupuestoMarcapaginas( @Valid @ModelAttribute PresupuestoMarcapaginas presupuestoMarcapaginas, BindingResult result, Locale locale) { if (result.hasErrors()) { // Construimos un mapa field -> mensaje para tu AJAX Map errores = result.getFieldErrors().stream() .collect(java.util.stream.Collectors.toMap( fe -> fe.getField(), fe -> fe.getDefaultMessage(), (a, b) -> a)); return ResponseEntity.badRequest().body(errores); } Map resultado = presupuestoService.getPrecioMarcapaginas(presupuestoMarcapaginas, locale); if ((Double) resultado.get("precio_total") == 0.0 && (Double) resultado.get("precio_unitario") == 0.0) { return ResponseEntity.badRequest() .body(messageSource.getMessage("presupuesto.errores.presupuesto-marcapaginas", null, locale)); } return ResponseEntity.ok(resultado); } // Se hace un post para no tener problemas con la longitud de la URL @PostMapping("/public/resumen") public ResponseEntity getResumen( @RequestBody Map body, Locale locale, HttpServletRequest request) { Presupuesto p = objectMapper.convertValue(body.get("presupuesto"), Presupuesto.class); Boolean save = objectMapper.convertValue(body.get("save"), Boolean.class); String mode = objectMapper.convertValue(body.get("mode"), String.class); @SuppressWarnings("unchecked") List> serviciosList = (List>) body.getOrDefault("servicios", List.of()); @SuppressWarnings("unchecked") Map datosMaquetacion = (Map) objectMapper .convertValue(body.get("datosMaquetacion"), Map.class); @SuppressWarnings("unchecked") Map datosMarcapaginas = (Map) objectMapper .convertValue(body.get("datosMarcapaginas"), Map.class); String sessionId = request.getSession(true).getId(); String ip = IpUtils.getClientIp(request); var resumen = presupuestoService.getResumen(p, serviciosList, datosMaquetacion, datosMarcapaginas, save, mode, locale, sessionId, ip); return ResponseEntity.ok(resumen); } // ============================================= // MÉTODOS PARA USUARIOS AUTENTICADOS // ============================================= @GetMapping public String getPresupuestoList(Model model, Authentication authentication, Locale locale) { List keys = List.of( "presupuesto.delete.title", "presupuesto.delete.text", "presupuesto.eliminar", "presupuesto.delete.button", "app.yes", "app.cancelar", "presupuesto.delete.ok.title", "presupuesto.delete.ok.text", "presupuesto.add.tipo", "presupuesto.add.anonimo", "presupuesto.add.cliente", "presupuesto.add.next", "presupuesto.add.cancel", "presupuesto.add.select-client", "presupuesto.add.error.options", "presupuesto.add.error.options-client"); Map translations = translationService.getTranslations(locale, keys); model.addAttribute("languageBundle", translations); return "imprimelibros/presupuestos/presupuesto-list"; } @GetMapping(value = { "/edit/{id}", "/view/{id}" }) public String getPresupuestoEditForm( @PathVariable(name = "id", required = true) Long id, RedirectAttributes redirectAttributes, Model model, Authentication authentication, Locale locale) { List keys = List.of( "presupuesto.plantilla-cubierta", "presupuesto.plantilla-cubierta-text", "presupuesto.impresion-cubierta", "presupuesto.impresion-cubierta-help", "presupuesto.exito.guardado", "presupuesto.add.error.save.title", "presupuesto.iva-reducido", "presupuesto.iva-reducido-descripcion"); Map translations = translationService.getTranslations(locale, keys); model.addAttribute("languageBundle", translations); model.addAttribute("pod", variableService.getValorEntero("POD")); model.addAttribute("ancho_alto_min", variableService.getValorEntero("ancho_alto_min")); model.addAttribute("ancho_alto_max", variableService.getValorEntero("ancho_alto_max")); // Buscar el presupuesto Optional presupuestoOpt = presupuestoRepository.findById(id); if (presupuestoOpt.isEmpty()) { // Añadir mensaje flash para mostrar alerta redirectAttributes.addFlashAttribute("errorMessage", messageSource.getMessage("presupuesto.errores.presupuesto-no-existe", new Object[] { id }, locale)); // Redirigir a la vista de lista return "redirect:/presupuesto"; } if(presupuestoOpt.get().getEstado() == Presupuesto.Estado.aceptado){ Map resumen = presupuestoService.getTextosResumen( presupuestoOpt.get(), Utils.decodeJsonList(presupuestoOpt.get().getServiciosJson()), Utils.decodeJsonMap(presupuestoOpt.get().getDatosMaquetacionJson()), Utils.decodeJsonMap(presupuestoOpt.get().getDatosMarcapaginasJson()), locale); model.addAttribute("resumen", resumen); model.addAttribute("presupuesto", presupuestoOpt.get()); return "imprimelibros/presupuestos/presupuestador-view"; } if (!presupuestoService.canAccessPresupuesto(presupuestoOpt.get(), authentication)) { // Añadir mensaje flash para mostrar alerta redirectAttributes.addFlashAttribute("errorMessage", messageSource.getMessage("app.errors.403", null, locale)); // Redirigir a la vista de lista return "redirect:/presupuesto"; } model.addAttribute("presupuesto_id", presupuestoOpt.get().getId()); String path = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()) .getRequest().getRequestURI(); String mode = path.contains("/view/") ? "view" : "edit"; if (mode.equals("view") || presupuestoOpt.get().getEstado() != Presupuesto.Estado.borrador) { model.addAttribute("appMode", "view"); } else { model.addAttribute("cliente_id", presupuestoOpt.get().getUser().getId()); model.addAttribute("appMode", "edit"); } model.addAttribute("id", presupuestoOpt.get().getId()); return "imprimelibros/presupuestos/presupuesto-form"; } @GetMapping(value = { "/add/{mode}", "/add/{mode}/{cliente_id}", "/add2/{cliente_id}" }) public String getPresupuestoEditForm( @PathVariable(name = "mode", required = false) String mode, @PathVariable(name = "cliente_id", required = false) Long clienteId, RedirectAttributes redirectAttributes, Model model, Authentication authentication, Locale locale) { List keys = List.of( "presupuesto.plantilla-cubierta", "presupuesto.plantilla-cubierta-text", "presupuesto.impresion-cubierta", "presupuesto.impresion-cubierta-help", "presupuesto.exito.guardado", "presupuesto.add.error.save.title", "presupuesto.iva-reducido", "presupuesto.iva-reducido-descripcion"); Map translations = translationService.getTranslations(locale, keys); model.addAttribute("languageBundle", translations); model.addAttribute("pod", variableService.getValorEntero("POD")); model.addAttribute("ancho_alto_min", variableService.getValorEntero("ancho_alto_min")); model.addAttribute("ancho_alto_max", variableService.getValorEntero("ancho_alto_max")); model.addAttribute("appMode", "add"); if (!mode.equals("public")) { model.addAttribute("cliente_id", clienteId); } model.addAttribute("mode", mode); return "imprimelibros/presupuestos/presupuesto-form"; } @GetMapping(value = "/datatable/{tipo}", produces = "application/json") @ResponseBody public DataTablesResponse> datatable( HttpServletRequest request, Authentication auth, Locale locale, @PathVariable("tipo") String tipo, Principal principal) { DataTablesRequest dt = DataTablesParser.from(request); if ("anonimos".equals(tipo)) { return dtService.datatablePublicos(dt, locale, principal); } else if ("clientes".equals(tipo)) { return dtService.datatablePrivados(dt, locale, principal); } else { throw new IllegalArgumentException("Tipo de datatable no válido"); } } @DeleteMapping("/{id}") @Transactional public ResponseEntity delete(@PathVariable Long id, Authentication auth, Locale locale) { Presupuesto p = presupuestoRepository.findById(id).orElse(null); if (p == null) { return ResponseEntity.status(HttpStatus.NOT_FOUND) .body(Map.of("message", messageSource.getMessage("presupuesto.error.not-found", null, locale))); } boolean isUser = auth != null && auth.getAuthorities().stream() .anyMatch(a -> a.getAuthority().equals("ROLE_USER")); Long ownerId = p.getUser() != null ? p.getUser().getId() : null; 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); } boolean isOwner = ownerId != null && ownerId.equals(currentUserId); if (isUser && !isOwner) { return ResponseEntity.status(HttpStatus.FORBIDDEN) .body(Map.of("message", messageSource.getMessage("presupuesto.error.delete-permission-denied", null, locale))); } if (p.getEstado() != null && !p.getEstado().equals(Presupuesto.Estado.borrador)) { return ResponseEntity.status(HttpStatus.FORBIDDEN) .body(Map.of("message", messageSource.getMessage("presupuesto.error.delete-not-draft", null, locale))); } try { p.setDeleted(true); p.setDeletedAt(Instant.now()); if (auth != null && auth.getPrincipal() instanceof UserDetailsImpl udi) { p.setDeletedBy(userRepo.getReferenceById(udi.getId())); } else if (auth != null) { userRepo.findByUserNameIgnoreCase(auth.getName()).ifPresent(p::setDeletedBy); } presupuestoRepository.saveAndFlush(p); return ResponseEntity.ok(Map.of("message", messageSource.getMessage("presupuesto.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("presupuesto.error.delete-internal-error", null, locale), "detail", ex.getClass().getSimpleName() + ": " + (ex.getMessage() != null ? ex.getMessage() : ""))); } } @GetMapping(value = "/api/get", produces = "application/json") public ResponseEntity getPresupuesto( @RequestParam("id") Long id, Authentication authentication) { Optional presupuestoOpt = presupuestoRepository.findById(id); if (!presupuestoService.canAccessPresupuesto(presupuestoOpt.get(), authentication)) { return ResponseEntity.status(403).build(); } if (presupuestoOpt.isPresent()) { PresupuestoFormDataDto vm = formDataMapper.toFormData(presupuestoOpt.get()); return ResponseEntity.ok(vm); } else { return ResponseEntity.notFound().build(); } } @PostMapping(path = "/api/save") public ResponseEntity save( @RequestBody Map body, Locale locale, HttpServletRequest request) { Presupuesto presupuesto = objectMapper.convertValue(body.get("presupuesto"), Presupuesto.class); Long id = objectMapper.convertValue(body.get("id"), Long.class); String mode = objectMapper.convertValue(body.get("mode"), String.class); @SuppressWarnings("unchecked") List> serviciosList = (List>) body.getOrDefault("servicios", List.of()); Long cliente_id = objectMapper.convertValue(body.get("cliente_id"), Long.class); Map datosMaquetacion = (Map) objectMapper .convertValue(body.get("datosMaquetacion"), Map.class); Map datosMarcapaginas = (Map) objectMapper .convertValue(body.get("datosMarcapaginas"), Map.class); Set> violations = validator.validate(presupuesto, PresupuestoValidationGroups.All.class); if (!violations.isEmpty()) { Map errores = new HashMap<>(); for (ConstraintViolation v : violations) { String campo = v.getPropertyPath().toString(); String mensaje = messageSource.getMessage(v.getMessage().replace("{", "").replace("}", ""), null, locale); errores.put(campo, mensaje); } return ResponseEntity.badRequest().body(errores); } try { Map saveResult = presupuestoService.guardarPresupuesto( presupuesto, serviciosList, datosMaquetacion, datosMarcapaginas, mode, cliente_id, id, request, locale); return ResponseEntity.ok(Map.of("id", saveResult.get("presupuesto_id"), "message", messageSource.getMessage("presupuesto.exito.guardado", null, locale))); } catch (Exception ex) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(Map.of("message", messageSource.getMessage("presupuesto.error.save-internal-error", null, locale), "detail", ex.getClass().getSimpleName() + ": " + (ex.getMessage() != null ? ex.getMessage() : ""))); } } }