terminado margenes presupuesto e incluido en la api

This commit is contained in:
2025-10-02 20:50:39 +02:00
parent 460d2cfc01
commit 1e24065fb7
18 changed files with 663 additions and 101 deletions

View File

@ -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;

View File

@ -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<String> keys = List.of();
List<String> keys = List.of(
"margenes-presupuesto.delete.title",
"margenes-presupuesto.delete.text",
"margenes-presupuesto.eliminar",
"margenes-presupuesto.delete.button",
"app.yes",
"app.cancelar",
"margenes-presupuesto.delete.ok.title",
"margenes-presupuesto.delete.ok.text");
Map<String, String> translations = translationService.getTranslations(locale, keys);
model.addAttribute("languageBundle", translations);
@ -57,23 +80,21 @@ public class MargenPresupuestoController {
public DataTablesResponse<Map<String, Object>> datatable(HttpServletRequest request, Authentication authentication,
Locale locale) {
DataTablesRequest dt = DataTablesParser.from(request); //
DataTablesRequest dt = DataTablesParser.from(request);
List<String> searchable = List.of(
"tipoEncuadernacion",
"tipoCubierta",
"id",
"tiradaMin", "tiradaMax",
"margenMin", "margenMax");
List<String> orderable = List.of(
"id",
"tipoEncuadernacion",
"tipoCubierta",
"tiradaMin",
"tiradaMax",
"margenMin",
"margenMax");
List<String> orderable = List.of(
"id",
"tipoEncuadernacion",
"tipoCubierta",
"tiradaMin",
"tiradaMax",
"margenMin",
"margenMax");
Specification<MargenPresupuesto> base = (root, query, cb) -> cb.conjunction();
long total = repo.count();
@ -85,13 +106,14 @@ public class MargenPresupuestoController {
.add("actions", (margen) -> {
return "<div class=\"hstack gap-3 flex-wrap\">\n" +
" <a href=\"javascript:void(0);\" data-id=\"" + margen.getId()
+ "\" class=\"link-success btn-edit-user fs-15\"><i class=\"ri-edit-2-line\"></i></a>\n"
+ "\" class=\"link-success btn-edit-margen fs-15\"><i class=\"ri-edit-2-line\"></i></a>\n"
+ " <a href=\"javascript:void(0);\" data-id=\"" + margen.getId()
+ "\" class=\"link-danger btn-delete-user fs-15\"><i class=\"ri-delete-bin-5-line\"></i></a>\n"
+ "\" class=\"link-danger btn-delete-margen fs-15\"><i class=\"ri-delete-bin-5-line\"></i></a>\n"
+ " </div>";
})
.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))));
}
}

View File

@ -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<MargenPresupuesto, Long>, JpaSpecificationExecutor<MargenPresupuesto> {
public interface MargenPresupuestoDao
extends JpaRepository<MargenPresupuesto, Long>, JpaSpecificationExecutor<MargenPresupuesto> {
@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);
}