diff --git a/src/main/java/com/imprimelibros/erp/config/BeanValidationConfig.java b/src/main/java/com/imprimelibros/erp/config/BeanValidationConfig.java new file mode 100644 index 0000000..1d72169 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/config/BeanValidationConfig.java @@ -0,0 +1,24 @@ +package com.imprimelibros.erp.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; +import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer; + +import jakarta.validation.ValidatorFactory; + +@Configuration +public class BeanValidationConfig { + + // Asegura que usamos la factory de Spring (con SpringConstraintValidatorFactory) + @Bean + public LocalValidatorFactoryBean validator() { + return new LocalValidatorFactoryBean(); + } + + // Inserta esa factory en Hibernate/JPA + @Bean + public HibernatePropertiesCustomizer hibernateValidationCustomizer(ValidatorFactory vf) { + return props -> props.put("jakarta.persistence.validation.factory", vf); + } +} diff --git a/src/main/java/com/imprimelibros/erp/configuracion/margenes_presupuestos/MargenPresupuesto.java b/src/main/java/com/imprimelibros/erp/configuracion/margenes_presupuestos/MargenPresupuesto.java index c64dc3b..c0c52f2 100644 --- a/src/main/java/com/imprimelibros/erp/configuracion/margenes_presupuestos/MargenPresupuesto.java +++ b/src/main/java/com/imprimelibros/erp/configuracion/margenes_presupuestos/MargenPresupuesto.java @@ -3,7 +3,6 @@ package com.imprimelibros.erp.configuracion.margenes_presupuestos; import jakarta.persistence.*; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import java.time.LocalDateTime; @@ -46,23 +45,23 @@ public class MargenPresupuesto { private TipoCubierta tipoCubierta; @Column(name="tirada_min", nullable = false) - @NotBlank(message="{validation.required}") + @NotNull(message="{validation.required}") @Min(value=1, message="{validation.min}") private Integer tiradaMin; @Column(name="tirada_max", nullable = false) - @NotBlank(message="{validation.required}") + @NotNull(message="{validation.required}") @Min(value=1, message="{validation.min}") private Integer tiradaMax; @Column(name="margen_max", nullable = false) - @NotBlank(message="{validation.required}") + @NotNull(message="{validation.required}") @Min(value = 0, message="{validation.min}") @Max(value = 200, message="{validation.max}") private Integer margenMax; @Column(name = "margen_min", nullable = false) - @NotBlank(message="{validation.required}") + @NotNull(message="{validation.required}") @Min(value = 0, message="{validation.min}") @Max(value = 200, message="{validation.max}") private Integer margenMin; diff --git a/src/main/java/com/imprimelibros/erp/configuracion/margenes_presupuestos/MargenPresupuestoController.java b/src/main/java/com/imprimelibros/erp/configuracion/margenes_presupuestos/MargenPresupuestoController.java index f0d47a4..d232b92 100644 --- a/src/main/java/com/imprimelibros/erp/configuracion/margenes_presupuestos/MargenPresupuestoController.java +++ b/src/main/java/com/imprimelibros/erp/configuracion/margenes_presupuestos/MargenPresupuestoController.java @@ -1,16 +1,27 @@ package com.imprimelibros.erp.configuracion.margenes_presupuestos; - +import java.time.LocalDateTime; +import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Optional; 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.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Controller; +import org.springframework.transaction.annotation.Transactional; +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.PutMapping; 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.datatables.DataTable; @@ -18,24 +29,28 @@ 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.presupuesto.Presupuesto.TipoCubierta; +import com.imprimelibros.erp.presupuesto.Presupuesto.TipoEncuadernacion; import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.util.Locale; import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; @Controller -@RequestMapping("/configuracion/margenes-presupuestos") +@RequestMapping("/configuracion/margenes-presupuesto") @PreAuthorize("hasRole('SUPERADMIN')") public class MargenPresupuestoController { - + private final MargenPresupuestoDao repo; private final TranslationService translationService; private final MessageSource messageSource; - public MargenPresupuestoController(MargenPresupuestoDao repo, TranslationService translationService - , MessageSource messageSource) { + public MargenPresupuestoController(MargenPresupuestoDao repo, TranslationService translationService, + MessageSource messageSource) { this.repo = repo; this.translationService = translationService; this.messageSource = messageSource; @@ -44,7 +59,15 @@ public class MargenPresupuestoController { @GetMapping() public String listView(Model model, Authentication authentication, Locale locale) { - List keys = List.of(); + List 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 translations = translationService.getTranslations(locale, keys); model.addAttribute("languageBundle", translations); @@ -57,23 +80,21 @@ public class MargenPresupuestoController { public DataTablesResponse> datatable(HttpServletRequest request, Authentication authentication, Locale locale) { - DataTablesRequest dt = DataTablesParser.from(request); // + DataTablesRequest dt = DataTablesParser.from(request); List searchable = List.of( - "tipoEncuadernacion", - "tipoCubierta", + "id", "tiradaMin", "tiradaMax", "margenMin", "margenMax"); - - List orderable = List.of( - "id", - "tipoEncuadernacion", - "tipoCubierta", - "tiradaMin", - "tiradaMax", - "margenMin", - "margenMax"); + List orderable = List.of( + "id", + "tipoEncuadernacion", + "tipoCubierta", + "tiradaMin", + "tiradaMax", + "margenMin", + "margenMax"); Specification base = (root, query, cb) -> cb.conjunction(); long total = repo.count(); @@ -85,13 +106,14 @@ public class MargenPresupuestoController { .add("actions", (margen) -> { return "
\n" + " \n" + + "\" class=\"link-success btn-edit-margen fs-15\">\n" + " \n" + + "\" class=\"link-danger btn-delete-margen fs-15\">\n" + "
"; }) .edit("tipoEncuadernacion", (margen) -> { - return messageSource.getMessage("presupuesto." + margen.getTipoEncuadernacion().name(),null, locale); + return messageSource.getMessage("presupuesto." + margen.getTipoEncuadernacion().name(), null, + locale); }) .edit("tipoCubierta", (margen) -> { return messageSource.getMessage("presupuesto." + margen.getTipoCubierta().name(), null, locale); @@ -99,25 +121,220 @@ public class MargenPresupuestoController { .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)); + String fEncuadernacion = Optional.ofNullable(req.raw.get("f_encuadernacion")).orElse("").trim(); + if (!fEncuadernacion.isEmpty()) { + boolean added = false; + // 1) Si llega el nombre del enum (p.ej. "fresado", "cosido", ...) + try { + var encEnum = TipoEncuadernacion.valueOf(fEncuadernacion); + builder.add((root, q, cb) -> cb.equal(root.get("tipoEncuadernacion"), encEnum)); + added = true; + } catch (IllegalArgumentException ignored) { + } + // 2) Si llega la clave i18n (p.ej. "presupuesto.fresado", ...) + if (!added) { + Arrays.stream(TipoEncuadernacion.values()) + .filter(e -> e.getMessageKey().equals(fEncuadernacion)) + .findFirst() + .ifPresent(encEnum -> builder + .add((root, q, cb) -> cb.equal(root.get("tipoEncuadernacion"), encEnum))); + } } - // 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); - }); - }*/ + // --- Cubierta --- + String fCubierta = Optional.ofNullable(req.raw.get("f_cubierta")).orElse("").trim(); + if (!fCubierta.isEmpty()) { + boolean added = false; + // 1) Si llega el nombre del enum (p.ej. "tapaBlanda", "tapaDura", + // "tapaDuraLomoRedondo") + try { + var cubEnum = TipoCubierta.valueOf(fCubierta); + builder.add((root, q, cb) -> cb.equal(root.get("tipoCubierta"), cubEnum)); + added = true; + } catch (IllegalArgumentException ignored) { + } + // 2) Si llega la clave i18n (p.ej. "presupuesto.tapa-blanda", ...) + if (!added) { + Arrays.stream(TipoCubierta.values()) + .filter(e -> e.getMessageKey().equals(fCubierta)) + .findFirst() + .ifPresent(cubEnum -> builder + .add((root, q, cb) -> cb.equal(root.get("tipoCubierta"), cubEnum))); + } + } }) .toJson(total); } - + + @GetMapping("form") + public String getForm(@RequestParam(required = false) Long id, + MargenPresupuesto margenPresupuesto, + BindingResult binding, + Model model, + HttpServletResponse response, + Locale locale) { + + if (id != null) { + var opt = repo.findById(id); + if (opt.isEmpty()) { + binding.reject("usuarios.error.noEncontrado", + messageSource.getMessage("usuarios.error.noEncontrado", null, locale)); + response.setStatus(404); + model.addAttribute("action", "/users/" + id); + return "imprimelibros/users/user-form :: userForm"; + } + + model.addAttribute("margenPresupuesto", opt.get()); + model.addAttribute("action", "/configuracion/margenes-presupuesto/" + id); + } else { + // Crear: valores por defecto + model.addAttribute("margenPresupuesto", new MargenPresupuesto()); + model.addAttribute("action", "/configuracion/margenes-presupuesto"); + } + return "imprimelibros/configuracion/margenes-presupuesto/margenes-presupuesto-form :: margenesPresupuestoForm"; + } + + @PostMapping + public String create( + MargenPresupuesto margenPresupuesto, + BindingResult binding, + Model model, + HttpServletResponse response, + Locale locale) { + + if (binding.hasErrors()) { + response.setStatus(422); + model.addAttribute("action", "/configuracion/margenes-presupuesto"); + return "imprimelibros/configuracion/margenes-presupuesto/margenes-presupuesto-form :: margenesPresupuestoForm"; + } + + MargenPresupuesto data = new MargenPresupuesto(); + data.setTipoEncuadernacion(margenPresupuesto.getTipoEncuadernacion()); + data.setTipoCubierta(margenPresupuesto.getTipoCubierta()); + data.setTiradaMin(margenPresupuesto.getTiradaMin()); + data.setTiradaMax(margenPresupuesto.getTiradaMax()); + data.setMargenMax(margenPresupuesto.getMargenMax()); + data.setMargenMin(margenPresupuesto.getMargenMin()); + + 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", "/configuracion/margenes-presupuesto"); + return "imprimelibros/configuracion/margenes-presupuesto/margenes-presupuesto-form :: margenesPresupuestoForm"; + } + response.setStatus(201); + return null; + } + + + @PutMapping("/{id}") + public String edit( + @PathVariable Long id, + MargenPresupuesto form, + BindingResult binding, + Model model, + HttpServletResponse response, + Locale locale) { + + var uOpt = repo.findById(id); + if (uOpt.isEmpty()) { + binding.reject("usuarios.error.noEncontrado", + messageSource.getMessage("usuarios.error.noEncontrado", null, locale)); + } + + if (binding.hasErrors()) { + response.setStatus(422); + model.addAttribute("action", "/configuracion/margenes-presupuesto/" + id); + return "imprimelibros/configuracion/margenes-presupuesto/margenes-presupuesto-form :: margenesPresupuestoForm"; + } + + var entity = uOpt.get(); + + // 3) Copiar solamente campos editables + entity.setTipoEncuadernacion(form.getTipoEncuadernacion()); + entity.setTipoCubierta(form.getTipoCubierta()); + entity.setTiradaMin(form.getTiradaMin()); + entity.setTiradaMax(form.getTiradaMax()); + 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); + return null; + } + + @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)))); + } } diff --git a/src/main/java/com/imprimelibros/erp/configuracion/margenes_presupuestos/MargenPresupuestoDao.java b/src/main/java/com/imprimelibros/erp/configuracion/margenes_presupuestos/MargenPresupuestoDao.java index a9d19cc..a85e679 100644 --- a/src/main/java/com/imprimelibros/erp/configuracion/margenes_presupuestos/MargenPresupuestoDao.java +++ b/src/main/java/com/imprimelibros/erp/configuracion/margenes_presupuestos/MargenPresupuestoDao.java @@ -8,21 +8,33 @@ import org.springframework.data.repository.query.Param; import com.imprimelibros.erp.presupuesto.Presupuesto.TipoEncuadernacion; import com.imprimelibros.erp.presupuesto.Presupuesto.TipoCubierta; -public interface MargenPresupuestoDao extends JpaRepository, JpaSpecificationExecutor { +public interface MargenPresupuestoDao + extends JpaRepository, JpaSpecificationExecutor { @Query(""" - SELECT COUNT(m) FROM MargenPresupuesto m - WHERE m.deleted = false - AND m.tipoEncuadernacion = :enc - AND m.tipoCubierta = :cub - AND (:id IS NULL OR m.id <> :id) - AND NOT (m.tiradaMax < :min OR m.tiradaMin > :max) - """) + SELECT COUNT(m) FROM MargenPresupuesto m + WHERE m.deleted = false + AND m.tipoEncuadernacion = :enc + AND m.tipoCubierta = :cub + AND (:id IS NULL OR m.id <> :id) + AND NOT (m.tiradaMax < :min OR m.tiradaMin > :max) + """) long countOverlaps( - @Param("enc") TipoEncuadernacion enc, - @Param("cub") TipoCubierta cub, - @Param("min") Integer min, - @Param("max") Integer max, - @Param("id") Long id - ); + @Param("enc") TipoEncuadernacion enc, + @Param("cub") TipoCubierta cub, + @Param("min") Integer min, + @Param("max") Integer max, + @Param("id") Long id); + + @Query(""" + SELECT m FROM MargenPresupuesto m + WHERE m.deleted = false + AND m.tipoEncuadernacion = :enc + AND m.tipoCubierta = :cub + AND :tirada BETWEEN m.tiradaMin AND m.tiradaMax + """) + MargenPresupuesto findByTipoAndTirada( + @Param("enc") TipoEncuadernacion enc, + @Param("cub") TipoCubierta cub, + @Param("tirada") Integer tirada); } diff --git a/src/main/java/com/imprimelibros/erp/externalApi/skApiClient.java b/src/main/java/com/imprimelibros/erp/externalApi/skApiClient.java index ecd259b..5f5cfb4 100644 --- a/src/main/java/com/imprimelibros/erp/externalApi/skApiClient.java +++ b/src/main/java/com/imprimelibros/erp/externalApi/skApiClient.java @@ -6,12 +6,19 @@ import org.springframework.stereotype.Service; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.imprimelibros.erp.configuracion.margenes_presupuestos.MargenPresupuesto; +import com.imprimelibros.erp.configuracion.margenes_presupuestos.MargenPresupuestoDao; +import com.imprimelibros.erp.presupuesto.Presupuesto.TipoCubierta; +import com.imprimelibros.erp.presupuesto.Presupuesto.TipoEncuadernacion; import java.util.Map; import java.util.HashMap; +import java.util.List; import java.util.function.Supplier; @Service @@ -22,13 +29,16 @@ public class skApiClient { private final AuthService authService; private final RestTemplate restTemplate; + private final MargenPresupuestoDao margenPresupuestoDao; - public skApiClient(AuthService authService) { + public skApiClient(AuthService authService, MargenPresupuestoDao margenPresupuestoDao) { this.authService = authService; this.restTemplate = new RestTemplate(); + this.margenPresupuestoDao = margenPresupuestoDao; } - public String getPrice(Map requestBody) { + public String getPrice(Map requestBody, TipoEncuadernacion tipoEncuadernacion, + TipoCubierta tipoCubierta) { return performWithRetry(() -> { String url = this.skApiUrl + "api/calcular"; @@ -45,14 +55,57 @@ public class skApiClient { String.class); try { - Map responseBody = new ObjectMapper().readValue(response.getBody(), Map.class); + Map responseBody = new ObjectMapper().readValue( + response.getBody(), + new TypeReference>() { + }); + + ObjectMapper mapper = new ObjectMapper(); if (responseBody.get("error") == null) { - return new ObjectMapper().writeValueAsString( - Map.of("data", responseBody.get("data"))); + Object dataObj = responseBody.get("data"); + + if (dataObj instanceof Map) { + @SuppressWarnings("unchecked") + Map data = (Map) dataObj; + + List tiradas = mapper.convertValue( + data.get("tiradas"), new TypeReference>() { + }); + List precios = mapper.convertValue( + data.get("precios"), new TypeReference>() { + }); + + for (int i = 0; i < tiradas.size(); i++) { + int tirada = tiradas.get(i); + + MargenPresupuesto margen = margenPresupuestoDao.findByTipoAndTirada( + tipoEncuadernacion, tipoCubierta, tirada); + + if (margen != null) { + double margenValue = calcularMargen( + tirada, + margen.getTiradaMin(), + margen.getTiradaMax(), + margen.getMargenMax(), + margen.getMargenMin()); + double nuevoPrecio = precios.get(i) * (1 + margenValue / 100.0); + precios.set(i, nuevoPrecio); + } else { + System.out.println("No se encontró margen para tirada " + tirada); + } + } + + // <-- Clave: sustituir la lista en el map que se devuelve + data.put("precios", precios); + // (tiradas no cambia, pero si la modificases: data.put("tiradas", tiradas);) + } + + return mapper.writeValueAsString(Map.of("data", responseBody.get("data"))); } else { return "{\"error\": 1}"; } + } catch (JsonProcessingException e) { e.printStackTrace(); return "{\"error\": 1}"; @@ -104,7 +157,11 @@ public class skApiClient { } catch (JsonProcessingException e) { // Fallback al 80% del ancho - Map tamanio = (Map) requestBody.get("tamanio"); + Map tamanio = new ObjectMapper().convertValue( + requestBody.get("tamanio"), + new TypeReference>() { + }); + if (tamanio == null || tamanio.get("ancho") == null) throw new RuntimeException("Tamaño no válido en la solicitud: " + requestBody); else { @@ -132,7 +189,10 @@ public class skApiClient { String.class); try { - Map responseBody = new ObjectMapper().readValue(response.getBody(), Map.class); + Map responseBody = new ObjectMapper().readValue( + response.getBody(), + new TypeReference>() { + }); return responseBody.get("data").toString(); } catch (JsonProcessingException e) { e.printStackTrace(); @@ -162,4 +222,14 @@ public class skApiClient { } } } -} + + private static double calcularMargen( + int tirada, int tiradaMin, int tiradaMax, + double margenMax, double margenMin) { + if (tirada <= tiradaMin) + return margenMax; + if (tirada >= tiradaMax) + return margenMin; + return margenMax - ((double) (tirada - tiradaMin) / (tiradaMax - tiradaMin)) * (margenMax - margenMin); + } +} \ No newline at end of file diff --git a/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoController.java b/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoController.java index 47a8f38..55ffb02 100644 --- a/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoController.java +++ b/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoController.java @@ -22,6 +22,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.http.MediaType; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.core.type.TypeReference; import com.imprimelibros.erp.externalApi.skApiClient; import com.imprimelibros.erp.presupuesto.classes.ImagenPresupuesto; import com.imprimelibros.erp.presupuesto.classes.PresupuestoMaquetacion; @@ -45,7 +46,7 @@ public class PresupuestoController { private final ObjectMapper objectMapper; - public PresupuestoController(ObjectMapper objectMapper){ + public PresupuestoController(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } @@ -178,7 +179,10 @@ public class PresupuestoController { // opciones gramaje interior resultado.putAll(presupuestoService.obtenerOpcionesGramajeInterior(presupuesto)); - List opciones = (List) resultado.get("opciones_gramaje_interior"); + + List opciones = new ObjectMapper().convertValue(resultado.get("opciones_papel_interior"), + new TypeReference>() { + }); if (opciones != null && !opciones.isEmpty()) { String gramajeActual = presupuesto.getGramajeInterior().toString(); @@ -206,7 +210,9 @@ public class PresupuestoController { } Map resultado = presupuestoService.obtenerOpcionesGramajeInterior(presupuesto); - List opciones = (List) resultado.get("opciones_gramaje_interior"); + List opciones = new ObjectMapper().convertValue(resultado.get("opciones_gramaje_interior"), + new TypeReference>() { + }); if (opciones != null && !opciones.isEmpty()) { String gramajeActual = presupuesto.getGramajeInterior().toString(); @@ -244,9 +250,12 @@ public class PresupuestoController { Map resultado = new HashMap<>(); Map papelesCubierta = presupuestoService.obtenerOpcionesPapelCubierta(presupuesto, locale); - List opciones = (List) presupuestoService - .obtenerOpcionesPapelCubierta(presupuesto, locale) - .get("opciones_papel_cubierta"); + 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())))) { @@ -255,7 +264,10 @@ public class PresupuestoController { resultado.putAll(papelesCubierta); resultado.putAll(presupuestoService.obtenerOpcionesGramajeCubierta(presupuesto)); - List gramajesCubierta = (List) resultado.get("opciones_gramaje_cubierta"); + 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)) { @@ -304,7 +316,8 @@ public class PresupuestoController { if (!errores.isEmpty()) { return ResponseEntity.badRequest().body(errores); } - String price = apiClient.getPrice(presupuestoService.toSkApiRequest(presupuesto)); + 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."); } diff --git a/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoService.java b/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoService.java index 5b4e095..43f1357 100644 --- a/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoService.java +++ b/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoService.java @@ -575,7 +575,7 @@ public class PresupuestoService { } else if (presupuestoTemp.getTipoImpresion() == Presupuesto.TipoImpresion.negro) { presupuestoTemp.setTipoImpresion(Presupuesto.TipoImpresion.negrohq); } - String priceStr = apiClient.getPrice(this.toSkApiRequest(presupuestoTemp)); + String priceStr = apiClient.getPrice(this.toSkApiRequest(presupuestoTemp), presupuestoTemp.getTipoEncuadernacion(), presupuestoTemp.getTipoCubierta()); Double price_prototipo = 0.0; try { price = new ObjectMapper().readValue(priceStr, new TypeReference<>() { @@ -846,7 +846,7 @@ public class PresupuestoService { public HashMap calcularPresupuesto(Presupuesto presupuesto, Locale locale) { HashMap price = new HashMap<>(); - String priceStr = apiClient.getPrice(this.toSkApiRequest(presupuesto)); + String priceStr = apiClient.getPrice(this.toSkApiRequest(presupuesto), presupuesto.getTipoEncuadernacion(), presupuesto.getTipoCubierta()); try { price = new ObjectMapper().readValue(priceStr, new TypeReference<>() { diff --git a/src/main/java/com/imprimelibros/erp/shared/validation/NoRangeOverlapValidator.java b/src/main/java/com/imprimelibros/erp/shared/validation/NoRangeOverlapValidator.java index daa87d7..25eda3d 100644 --- a/src/main/java/com/imprimelibros/erp/shared/validation/NoRangeOverlapValidator.java +++ b/src/main/java/com/imprimelibros/erp/shared/validation/NoRangeOverlapValidator.java @@ -5,15 +5,21 @@ import java.beans.PropertyDescriptor; import java.lang.reflect.Method; import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.FlushModeType; import jakarta.persistence.PersistenceContext; +import jakarta.persistence.PersistenceUnit; import jakarta.persistence.criteria.*; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; +import org.springframework.stereotype.Component; + +@Component public class NoRangeOverlapValidator implements ConstraintValidator { - @PersistenceContext - private EntityManager em; + @PersistenceUnit + private EntityManagerFactory emf; private String minField; private String maxField; @@ -38,22 +44,28 @@ public class NoRangeOverlapValidator implements ConstraintValidator entityClass = bean.getClass(); Number min = (Number) read(bean, minField); Number max = (Number) read(bean, maxField); Object id = safeRead(bean, idField); // puede ser null en INSERT - if (min == null || max == null) return true; + if (min == null || max == null) + return true; if (min.longValue() > max.longValue()) { ctx.disableDefaultConstraintViolation(); ctx.buildConstraintViolationWithTemplate(invalidRangeMessage) - .addPropertyNode(maxField) - .addConstraintViolation(); + .addPropertyNode(maxField) + .addConstraintViolation(); return false; } @@ -82,9 +94,8 @@ public class NoRangeOverlapValidator implements ConstraintValidator eMax = root.get(maxField); Predicate noOverlap = cb.or( - cb.lt(eMax.as(Long.class), min.longValue()), - cb.gt(eMin.as(Long.class), max.longValue()) - ); + cb.lt(eMax.as(Long.class), min.longValue()), + cb.gt(eMin.as(Long.class), max.longValue())); Predicate overlap = cb.not(noOverlap); cq.where(cb.and(pred, overlap)); @@ -93,9 +104,9 @@ public class NoRangeOverlapValidator implements ConstraintValidator 0) { ctx.disableDefaultConstraintViolation(); ctx.buildConstraintViolationWithTemplate(message) - .addPropertyNode(minField).addConstraintViolation(); + .addPropertyNode(minField).addConstraintViolation(); ctx.buildConstraintViolationWithTemplate(message) - .addPropertyNode(maxField).addConstraintViolation(); + .addPropertyNode(maxField).addConstraintViolation(); return false; } diff --git a/src/main/resources/i18n/margenesPresupuesto_es.properties b/src/main/resources/i18n/margenesPresupuesto_es.properties index 7ef7148..9a4799b 100644 --- a/src/main/resources/i18n/margenesPresupuesto_es.properties +++ b/src/main/resources/i18n/margenesPresupuesto_es.properties @@ -1,6 +1,9 @@ margenes-presupuesto.titulo=Márgenes de presupuesto margenes-presupuesto.breadcrumb=Márgenes de presupuesto margenes-presupuesto.add=Añadir +margenes-presupuesto.nuevo=Nuevo margen +margenes-presupuesto.editar=Editar margen +margenes-presupuesto.eliminar=Eliminar margenes-presupuesto.tabla.id=ID margenes-presupuesto.tabla.tipo_encuadernacion=Tipo encuadernación @@ -11,4 +14,22 @@ margenes-presupuesto.tabla.margen_minimo=Margen Mín. margenes-presupuesto.tabla.margen_maximo=Margen Máx. margenes-presupuesto.tabla.acciones=Acciones -margenes-presupuesto.todos=Todos \ No newline at end of file +margenes-presupuesto.form.tipo_encuadernacion=Tipo de encuadernación +margenes-presupuesto.form.tipo_cubierta=Tipo de cubierta +margenes-presupuesto.form.tirada_minima=Tirada mínima +margenes-presupuesto.form.tirada_maxima=Tirada máxima +margenes-presupuesto.form.margen_minimo=Margen mínimo (%) +margenes-presupuesto.form.margen_maximo=Margen máximo (%) + +margenes-presupuesto.todos=Todos + +margenes-presupuesto.delete.title=Eliminar margen +margenes-presupuesto.delete.button=Si, ELIMINAR +margenes-presupuesto.delete.text=¿Está seguro de que desea eliminar este margen?
Esta acción no se puede deshacer. +margenes-presupuesto.delete.ok.title=Margen eliminado +margenes-presupuesto.delete.ok.text=El margen ha sido eliminado con éxito. + +margenes-presupuesto.exito.eliminado=Margen eliminado con éxito. + +margenes-presupuesto.error.delete-internal-error=No se puede eliminar: error interno. +margenes-presupuesto.error.delete-not-found=No se puede eliminar: margen no encontrado. \ No newline at end of file diff --git a/src/main/resources/i18n/presupuesto_es.properties b/src/main/resources/i18n/presupuesto_es.properties index ff2dd67..38984bf 100644 --- a/src/main/resources/i18n/presupuesto_es.properties +++ b/src/main/resources/i18n/presupuesto_es.properties @@ -42,6 +42,7 @@ presupuesto.grapado-descripcion=Grapado (entre 12 y 40 páginas) presupuesto.espiral=Espiral presupuesto.espiral-descripcion=Espiral (a partir de 20 páginas) presupuesto.wire-o=Wire-O +presupuesto.wireo=Wire-O presupuesto.wire-o-descripcion=Wire-O (a partir de 20 páginas) presupuesto.encuadernacion-descripcion=Seleccione la encuadernación del libro presupuesto.continuar-interior=Continuar a diseño interior diff --git a/src/main/resources/i18n/validation_es.properties b/src/main/resources/i18n/validation_es.properties index 35a5d87..489d7fa 100644 --- a/src/main/resources/i18n/validation_es.properties +++ b/src/main/resources/i18n/validation_es.properties @@ -8,4 +8,5 @@ validation.unique=El valor ya existe y debe ser único validation.email=El correo electrónico no es válido validation.range.overlaps=El rango se solapa con otro existente. validation.range.invalid=El valor máximo debe ser mayor o igual que el mínimo. - +validation.range.invalid2=Rango no válido. +validation.db=Error de base de datos: {message} diff --git a/src/main/resources/static/assets/images/cover-bg-login.png b/src/main/resources/static/assets/images/cover-bg-login.png index 3e2159b..3262029 100644 Binary files a/src/main/resources/static/assets/images/cover-bg-login.png and b/src/main/resources/static/assets/images/cover-bg-login.png differ diff --git a/src/main/resources/static/assets/js/pages/imprimelibros/configuracion/margenes-presupuesto/list.js b/src/main/resources/static/assets/js/pages/imprimelibros/configuracion/margenes-presupuesto/list.js index 7e3119a..2e71a67 100644 --- a/src/main/resources/static/assets/js/pages/imprimelibros/configuracion/margenes-presupuesto/list.js +++ b/src/main/resources/static/assets/js/pages/imprimelibros/configuracion/margenes-presupuesto/list.js @@ -18,18 +18,20 @@ return; } - new DataTable('#margenes-datatable', { + const table = new DataTable('#margenes-datatable', { processing: true, serverSide: true, orderCellsTop: true, + stateSave: true, pageLength: 50, language: { url: '/assets/libs/datatables/i18n/' + language + '.json' }, responsive: true, ajax: { - url: '/configuracion/margenes-presupuestos/datatable', + url: '/configuracion/margenes-presupuesto/datatable', method: 'GET', data: function (d) { - // filtros si los necesitas + d.f_encuadernacion = $('#search-encuadernacion').val() || ''; // 'USER' | 'ADMIN' | 'SUPERADMIN' | '' + d.f_cubierta = $('#search-cubierta').val() || ''; // 'true' | 'false' | '' } }, order: [[0, 'asc']], @@ -45,4 +47,126 @@ ], columnDefs: [{ targets: -1, orderable: false, searchable: false }] }); + + table.on("keyup", ".margenes-presupuesto-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", ".margenes-presupuesto-select-filter", function () { + table.draw(); + }); + + const modalEl = document.getElementById('margenesPresupuestoFormModal'); + const modal = bootstrap.Modal.getOrCreateInstance(modalEl); + + // Abrir "Crear" + $('#addButton').on('click', (e) => { + e.preventDefault(); + $.get('/configuracion/margenes-presupuesto/form', function (html) { + $('#margenesPresupuestoModalBody').html(html); + const title = $('#margenesPresupuestoModalBody #margenesPresupuestoForm').data('add'); + $('#margenesPresupuestoModal .modal-title').text(title); + modal.show(); + }); + }); + + // Abrir "Editar" + $(document).on('click', '.btn-edit-margen', function (e) { + e.preventDefault(); + const id = $(this).data('id'); + $.get('/configuracion/margenes-presupuesto/form', { id }, function (html) { + $('#margenesPresupuestoModalBody').html(html); + const title = $('#margenesPresupuestoModalBody #margenesPresupuestoForm').data('edit'); + $('#margenesPresupuestoModal .modal-title').text(title); + modal.show(); + }); + }); + + // Botón "Eliminar" + $(document).on('click', '.btn-delete-margen', function (e) { + e.preventDefault(); + const id = $(this).data('id'); + + Swal.fire({ + title: window.languageBundle.get(['margenes-presupuesto.delete.title']) || 'Eliminar margen', + html: window.languageBundle.get(['margenes-presupuesto.delete.text']) || 'Esta acción no se puede deshacer.', + icon: 'warning', + showCancelButton: true, + buttonsStyling: false, + customClass: { + confirmButton: 'btn btn-danger w-xs mt-2', + cancelButton: 'btn btn-light w-xs mt-2' + }, + confirmButtonText: window.languageBundle.get(['margenes-presupuesto.delete.button']) || 'Eliminar', + cancelButtonText: window.languageBundle.get(['app.cancelar']) || 'Cancelar', + }).then((result) => { + if (!result.isConfirmed) return; + + $.ajax({ + url: '/configuracion/margenes-presupuesto/' + id, + type: 'DELETE', + success: function () { + Swal.fire({ + icon: 'success', title: window.languageBundle.get(['margenes-presupuesto.delete.ok.title']) || 'Eliminado', + text: window.languageBundle.get(['margenes-presupuesto.delete.ok.text']) || 'El margen ha sido eliminado con éxito.', + showConfirmButton: true, + customClass: { + confirmButton: 'btn btn-secondary w-xs mt-2', + }, + }); + $('#margenes-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', '#margenesPresupuestoForm', function (e) { + e.preventDefault(); + const $form = $(this); + + $.ajax({ + url: $form.attr('action'), + type: 'POST', // PUT simulado via _method + data: $form.serialize(), + dataType: 'html', + success: function (html) { + // Si por cualquier motivo llega 200 con fragmento, lo insertamos igual + if (typeof html === 'string' && html.indexOf('id="margenesPresupuestoForm"') !== -1 && html.indexOf(' 0; + const title = $('#margenesPresupuestoModalBody #margenesPresupuestoForm').data(isEdit ? 'edit' : 'add'); + $('#margenesPresupuestoModal .modal-title').text(title); + return; + } + // Éxito real: cerrar y recargar tabla + modal.hide(); + table.ajax.reload(null, false); + }, + error: function (xhr) { + // Con 422 devolvemos el fragmento con errores aquí + if (xhr.status === 422 && xhr.responseText) { + $('#margenesPresupuestoModalBody').html(xhr.responseText); + const isEdit = $('#margenesPresupuestoModalBody #margenesPresupuestoForm input[name="_method"][value="PUT"]').length > 0; + const title = $('#margenesPresupuestoModalBody #margenesPresupuestoForm').data(isEdit ? 'edit' : 'add'); + $('#margenesPresupuestoModal .modal-title').text(title); + return; + } + // Fallback + $('#margenesPresupuestoModalBody').html('
Error inesperado.
'); + } + }); + }); + })(); diff --git a/src/main/resources/static/assets/js/pages/imprimelibros/users/list.js b/src/main/resources/static/assets/js/pages/imprimelibros/users/list.js index 3e1096c..8d5abb5 100644 --- a/src/main/resources/static/assets/js/pages/imprimelibros/users/list.js +++ b/src/main/resources/static/assets/js/pages/imprimelibros/users/list.js @@ -39,7 +39,7 @@ $(() => { columnDefs: [{ targets: -1, orderable: false, searchable: false }] }); - table.on("keyup", ".user-filter", function () { + table.on("keyup", ".user-filter", function() { const colName = $(this).data("col"); const colIndex = table.settings()[0].aoColumns.findIndex(c => c.name === colName); @@ -48,7 +48,7 @@ $(() => { } }); - table.on("change", ".user-filter-select", function () { + table.on("change", ".user-filter-select", function() { table.draw(); }); diff --git a/src/main/resources/templates/imprimelibros/configuracion/margenes-presupuesto/margenes-presupuesto-form.html b/src/main/resources/templates/imprimelibros/configuracion/margenes-presupuesto/margenes-presupuesto-form.html new file mode 100644 index 0000000..fb05e93 --- /dev/null +++ b/src/main/resources/templates/imprimelibros/configuracion/margenes-presupuesto/margenes-presupuesto-form.html @@ -0,0 +1,67 @@ +
+
+ + + +
+
+
+ +
+ + +
Error
+
+ +
+ + +
Error
+
+ +
+ + +
Error
+
+ +
+ + +
Error
+
+ +
+ + +
Error
+
+ +
+ + +
Error
+
+ +
+ +
+
+
\ No newline at end of file diff --git a/src/main/resources/templates/imprimelibros/configuracion/margenes-presupuesto/margenes-presupuesto-list.html b/src/main/resources/templates/imprimelibros/configuracion/margenes-presupuesto/margenes-presupuesto-list.html index bae6451..0d8e169 100644 --- a/src/main/resources/templates/imprimelibros/configuracion/margenes-presupuesto/margenes-presupuesto-list.html +++ b/src/main/resources/templates/imprimelibros/configuracion/margenes-presupuesto/margenes-presupuesto-list.html @@ -23,7 +23,7 @@
+ th:replace="imprimelibros/partials/modal-form :: modal('margenesPresupuestoFormModal', 'margenes-presupuesto.add', 'modal-md', 'margenesPresupuestoModalBody')">