mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-01-12 16:38:48 +00:00
añadidos margenes presupuesto
This commit is contained in:
@ -4,7 +4,6 @@ import javax.sql.DataSource;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.MediaType;
|
||||
@ -24,9 +23,6 @@ import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
|
||||
import com.imprimelibros.erp.users.User;
|
||||
import com.imprimelibros.erp.users.UserDao;
|
||||
import com.imprimelibros.erp.users.UserDetailsImpl;
|
||||
import com.imprimelibros.erp.users.UserServiceImpl;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
@ -0,0 +1,182 @@
|
||||
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;
|
||||
|
||||
import org.hibernate.annotations.SQLDelete;
|
||||
import org.hibernate.annotations.SQLRestriction;
|
||||
import com.imprimelibros.erp.presupuesto.Presupuesto.TipoEncuadernacion;
|
||||
import com.imprimelibros.erp.presupuesto.Presupuesto.TipoCubierta;
|
||||
import com.imprimelibros.erp.shared.validation.NoRangeOverlap;
|
||||
|
||||
|
||||
@NoRangeOverlap(
|
||||
min = "tiradaMin",
|
||||
max = "tiradaMax",
|
||||
id = "id",
|
||||
partitionBy = {"tipoEncuadernacion","tipoCubierta"},
|
||||
deletedFlag = "deleted", // <- si usas soft delete
|
||||
deletedActiveValue = false, // activo cuando deleted == false
|
||||
message = "{validation.range.overlaps}",
|
||||
invalidRangeMessage = "{validation.range.invalid}"
|
||||
)
|
||||
@Entity
|
||||
@Table(name = "margenes_presupuesto")
|
||||
@SQLDelete(sql = "UPDATE margenes_presupuesto SET deleted = TRUE, deleted_at = NOW() WHERE id = ?")
|
||||
@SQLRestriction("deleted = false")
|
||||
public class MargenPresupuesto {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name="tipo_encuadernacion", nullable = false, length = 50)
|
||||
@NotNull(message="{validation.required}")
|
||||
@Enumerated(EnumType.STRING)
|
||||
private TipoEncuadernacion tipoEncuadernacion;
|
||||
|
||||
@Column(name="tipo_cubierta", nullable = false, length = 50)
|
||||
@NotNull(message="{validation.required}")
|
||||
@Enumerated(EnumType.STRING)
|
||||
private TipoCubierta tipoCubierta;
|
||||
|
||||
@Column(name="tirada_min", nullable = false)
|
||||
@NotBlank(message="{validation.required}")
|
||||
@Min(value=1, message="{validation.min}")
|
||||
private Integer tiradaMin;
|
||||
|
||||
@Column(name="tirada_max", nullable = false)
|
||||
@NotBlank(message="{validation.required}")
|
||||
@Min(value=1, message="{validation.min}")
|
||||
private Integer tiradaMax;
|
||||
|
||||
@Column(name="margen_max", nullable = false)
|
||||
@NotBlank(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}")
|
||||
@Min(value = 0, message="{validation.min}")
|
||||
@Max(value = 200, message="{validation.max}")
|
||||
private Integer margenMin;
|
||||
|
||||
@Column(name="created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name="updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Column(nullable = false)
|
||||
private boolean deleted = false;
|
||||
|
||||
@Column(name="deleted_at")
|
||||
private LocalDateTime deletedAt;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public TipoEncuadernacion getTipoEncuadernacion() {
|
||||
return tipoEncuadernacion;
|
||||
}
|
||||
|
||||
public void setTipoEncuadernacion(TipoEncuadernacion tipoEncuadernacion) {
|
||||
this.tipoEncuadernacion = tipoEncuadernacion;
|
||||
}
|
||||
|
||||
public TipoCubierta getTipoCubierta() {
|
||||
return tipoCubierta;
|
||||
}
|
||||
|
||||
public void setTipoCubierta(TipoCubierta tipoCubierta) {
|
||||
this.tipoCubierta = tipoCubierta;
|
||||
}
|
||||
|
||||
public Integer getTiradaMin() {
|
||||
return tiradaMin;
|
||||
}
|
||||
|
||||
public void setTiradaMin(Integer tiradaMin) {
|
||||
this.tiradaMin = tiradaMin;
|
||||
}
|
||||
|
||||
public Integer getTiradaMax() {
|
||||
return tiradaMax;
|
||||
}
|
||||
|
||||
public void setTiradaMax(Integer tiradaMax) {
|
||||
this.tiradaMax = tiradaMax;
|
||||
}
|
||||
|
||||
public Integer getMargenMax() {
|
||||
return margenMax;
|
||||
}
|
||||
|
||||
public void setMargenMax(Integer margenMax) {
|
||||
this.margenMax = margenMax;
|
||||
}
|
||||
|
||||
public Integer getMargenMin() {
|
||||
return margenMin;
|
||||
}
|
||||
|
||||
public void setMargenMin(Integer margenMin) {
|
||||
this.margenMin = margenMin;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public boolean isDeleted() {
|
||||
return deleted;
|
||||
}
|
||||
|
||||
public void setDeleted(boolean deleted) {
|
||||
this.deleted = deleted;
|
||||
}
|
||||
|
||||
public LocalDateTime getDeletedAt() {
|
||||
return deletedAt;
|
||||
}
|
||||
|
||||
public void setDeletedAt(LocalDateTime deletedAt) {
|
||||
this.deletedAt = deletedAt;
|
||||
}
|
||||
|
||||
@PrePersist
|
||||
void onCreate() {
|
||||
this.createdAt = LocalDateTime.now();
|
||||
this.updatedAt = this.createdAt;
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
void onUpdate() {
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,123 @@
|
||||
package com.imprimelibros.erp.configuracion.margenes_presupuestos;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
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 jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import org.springframework.ui.Model;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/configuracion/margenes-presupuestos")
|
||||
@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) {
|
||||
this.repo = repo;
|
||||
this.translationService = translationService;
|
||||
this.messageSource = messageSource;
|
||||
}
|
||||
|
||||
@GetMapping()
|
||||
public String listView(Model model, Authentication authentication, Locale locale) {
|
||||
|
||||
List<String> keys = List.of();
|
||||
|
||||
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||
model.addAttribute("languageBundle", translations);
|
||||
|
||||
return "imprimelibros/configuracion/margenes-presupuesto/margenes-presupuesto-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); //
|
||||
|
||||
List<String> searchable = List.of(
|
||||
"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();
|
||||
|
||||
return DataTable
|
||||
.of(repo, MargenPresupuesto.class, dt, searchable) // 'searchable' en DataTable.java
|
||||
// edita columnas "reales":
|
||||
.orderable(orderable)
|
||||
.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"
|
||||
+ " <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"
|
||||
+ " </div>";
|
||||
})
|
||||
.edit("tipoEncuadernacion", (margen) -> {
|
||||
return messageSource.getMessage("presupuesto." + margen.getTipoEncuadernacion().name(),null, locale);
|
||||
})
|
||||
.edit("tipoCubierta", (margen) -> {
|
||||
return messageSource.getMessage("presupuesto." + margen.getTipoCubierta().name(), null, locale);
|
||||
})
|
||||
.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));
|
||||
}
|
||||
|
||||
// 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);
|
||||
});
|
||||
}*/
|
||||
})
|
||||
.toJson(total);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package com.imprimelibros.erp.configuracion.margenes_presupuestos;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
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> {
|
||||
|
||||
@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)
|
||||
""")
|
||||
long countOverlaps(
|
||||
@Param("enc") TipoEncuadernacion enc,
|
||||
@Param("cub") TipoCubierta cub,
|
||||
@Param("min") Integer min,
|
||||
@Param("max") Integer max,
|
||||
@Param("id") Long id
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
package com.imprimelibros.erp.configuracion.margenes_presupuestos;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.imprimelibros.erp.presupuesto.Presupuesto.TipoEncuadernacion;
|
||||
import com.imprimelibros.erp.presupuesto.Presupuesto.TipoCubierta;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
public class MargenPresupuestoService {
|
||||
|
||||
private final MargenPresupuestoDao dao;
|
||||
|
||||
public MargenPresupuestoService(MargenPresupuestoDao dao) {
|
||||
this.dao = dao;
|
||||
}
|
||||
|
||||
public List<MargenPresupuesto> findAll() {
|
||||
return dao.findAll();
|
||||
}
|
||||
|
||||
public Optional<MargenPresupuesto> findById(Long id) {
|
||||
return dao.findById(id);
|
||||
}
|
||||
|
||||
public MargenPresupuesto save(MargenPresupuesto entity) {
|
||||
return dao.save(entity);
|
||||
}
|
||||
|
||||
public void delete(Long id) {
|
||||
dao.deleteById(id);
|
||||
}
|
||||
|
||||
|
||||
public boolean hasOverlap(TipoEncuadernacion enc, TipoCubierta cub, Integer min, Integer max, Long excludeId) {
|
||||
long count = dao.countOverlaps(enc, cub, min, max, excludeId);
|
||||
return count > 0;
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,7 @@
|
||||
package com.imprimelibros.erp.datatables;
|
||||
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.data.domain.*;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
@ -26,7 +28,9 @@ public class DataTable<T> {
|
||||
private final List<Function<Map<String, Object>, Map<String, Object>>> editors = new ArrayList<>();
|
||||
private final List<FilterHook<T>> filters = new ArrayList<>();
|
||||
private Specification<T> baseSpec = (root, q, cb) -> cb.conjunction();
|
||||
private final ObjectMapper om = new ObjectMapper();
|
||||
private final ObjectMapper om = new ObjectMapper()
|
||||
.registerModule(new JavaTimeModule())
|
||||
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||
private List<String> orderable = null;
|
||||
|
||||
private DataTable(JpaSpecificationExecutor<T> repo, Class<T> entityClass, DataTablesRequest dt,
|
||||
|
||||
@ -0,0 +1,35 @@
|
||||
package com.imprimelibros.erp.shared.validation;
|
||||
|
||||
import static java.lang.annotation.ElementType.TYPE;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
import jakarta.validation.Constraint;
|
||||
import jakarta.validation.Payload;
|
||||
|
||||
@Documented
|
||||
@Target(TYPE)
|
||||
@Retention(RUNTIME)
|
||||
@Constraint(validatedBy = NoRangeOverlapValidator.class)
|
||||
public @interface NoRangeOverlap {
|
||||
|
||||
// Campos obligatorios
|
||||
String min(); // nombre del campo min (Integer/Long/etc.)
|
||||
String max(); // nombre del campo max
|
||||
|
||||
// Campos opcionales
|
||||
String id() default "id"; // nombre del campo ID (para excluir self en update)
|
||||
String[] partitionBy() default {}; // ej. {"tipoEncuadernacion","tipoCubierta"}
|
||||
|
||||
// Soft delete opcional
|
||||
String deletedFlag() default ""; // ej. "deleted" (si vacío, no se aplica filtro)
|
||||
boolean deletedActiveValue() default false; // qué valor significa "activo" (normalmente false)
|
||||
|
||||
// Mensajes
|
||||
String message() default "{validation.range.overlaps}";
|
||||
String invalidRangeMessage() default "{validation.range.invalid}";
|
||||
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
@ -0,0 +1,127 @@
|
||||
package com.imprimelibros.erp.shared.validation;
|
||||
|
||||
import java.beans.IntrospectionException;
|
||||
import java.beans.PropertyDescriptor;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
import jakarta.persistence.criteria.*;
|
||||
import jakarta.validation.ConstraintValidator;
|
||||
import jakarta.validation.ConstraintValidatorContext;
|
||||
|
||||
public class NoRangeOverlapValidator implements ConstraintValidator<NoRangeOverlap, Object> {
|
||||
|
||||
@PersistenceContext
|
||||
private EntityManager em;
|
||||
|
||||
private String minField;
|
||||
private String maxField;
|
||||
private String idField;
|
||||
private String[] partitionFields;
|
||||
private String deletedFlag;
|
||||
private boolean deletedActiveValue;
|
||||
private String message;
|
||||
private String invalidRangeMessage;
|
||||
|
||||
@Override
|
||||
public void initialize(NoRangeOverlap ann) {
|
||||
this.minField = ann.min();
|
||||
this.maxField = ann.max();
|
||||
this.idField = ann.id();
|
||||
this.partitionFields = ann.partitionBy();
|
||||
this.deletedFlag = ann.deletedFlag();
|
||||
this.deletedActiveValue = ann.deletedActiveValue();
|
||||
this.message = ann.message();
|
||||
this.invalidRangeMessage = ann.invalidRangeMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid(Object bean, ConstraintValidatorContext ctx) {
|
||||
if (bean == null) return true;
|
||||
|
||||
try {
|
||||
Class<?> 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.longValue() > max.longValue()) {
|
||||
ctx.disableDefaultConstraintViolation();
|
||||
ctx.buildConstraintViolationWithTemplate(invalidRangeMessage)
|
||||
.addPropertyNode(maxField)
|
||||
.addConstraintViolation();
|
||||
return false;
|
||||
}
|
||||
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaQuery<Long> cq = cb.createQuery(Long.class);
|
||||
Root<?> root = cq.from(entityClass);
|
||||
|
||||
cq.select(cb.count(root));
|
||||
|
||||
Predicate pred = cb.conjunction();
|
||||
|
||||
if (id != null) {
|
||||
pred = cb.and(pred, cb.notEqual(root.get(idField), id));
|
||||
}
|
||||
|
||||
for (String pf : partitionFields) {
|
||||
Object val = read(bean, pf);
|
||||
pred = cb.and(pred, cb.equal(root.get(pf), val));
|
||||
}
|
||||
|
||||
if (!deletedFlag.isEmpty()) {
|
||||
pred = cb.and(pred, cb.equal(root.get(deletedFlag), deletedActiveValue));
|
||||
}
|
||||
|
||||
Expression<Number> eMin = root.get(minField);
|
||||
Expression<Number> eMax = root.get(maxField);
|
||||
|
||||
Predicate noOverlap = cb.or(
|
||||
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));
|
||||
|
||||
Long count = em.createQuery(cq).getSingleResult();
|
||||
if (count != null && count > 0) {
|
||||
ctx.disableDefaultConstraintViolation();
|
||||
ctx.buildConstraintViolationWithTemplate(message)
|
||||
.addPropertyNode(minField).addConstraintViolation();
|
||||
ctx.buildConstraintViolationWithTemplate(message)
|
||||
.addPropertyNode(maxField).addConstraintViolation();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
} catch (Exception ex) {
|
||||
// En caso de error inesperado, puedes loguear aquí
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private Object read(Object bean, String name) throws Exception {
|
||||
PropertyDescriptor pd = getPropertyDescriptor(bean.getClass(), name);
|
||||
Method getter = pd.getReadMethod();
|
||||
return getter.invoke(bean);
|
||||
}
|
||||
|
||||
private Object safeRead(Object bean, String name) {
|
||||
try {
|
||||
return read(bean, name);
|
||||
} catch (Exception ignore) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private PropertyDescriptor getPropertyDescriptor(Class<?> clazz, String name) throws IntrospectionException {
|
||||
return new PropertyDescriptor(name, clazz);
|
||||
}
|
||||
}
|
||||
@ -8,7 +8,6 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
@ -69,3 +69,8 @@ spring.mail.username=no-reply@imprimelibros.com
|
||||
spring.mail.password=%j4Su*#ZcjRDYsa$
|
||||
spring.mail.properties.mail.smtp.auth=true
|
||||
spring.mail.properties.mail.smtp.starttls.enable=true
|
||||
|
||||
#
|
||||
# Remove JSESSIONID from URL
|
||||
#
|
||||
server.servlet.session.persistent=false
|
||||
@ -0,0 +1 @@
|
||||
|
||||
14
src/main/resources/i18n/margenesPresupuesto_es.properties
Normal file
14
src/main/resources/i18n/margenesPresupuesto_es.properties
Normal file
@ -0,0 +1,14 @@
|
||||
margenes-presupuesto.titulo=Márgenes de presupuesto
|
||||
margenes-presupuesto.breadcrumb=Márgenes de presupuesto
|
||||
margenes-presupuesto.add=Añadir
|
||||
|
||||
margenes-presupuesto.tabla.id=ID
|
||||
margenes-presupuesto.tabla.tipo_encuadernacion=Tipo encuadernación
|
||||
margenes-presupuesto.tabla.tipo_cubierta=Tipo cubierta
|
||||
margenes-presupuesto.tabla.tirada_minima=Tirada Mín.
|
||||
margenes-presupuesto.tabla.tirada_maxima=Tirada Máx.
|
||||
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
|
||||
@ -71,8 +71,11 @@ presupuesto.plantilla-cubierta-text=Recuerde que la cubierta es el conjunto form
|
||||
presupuesto.tipo-cubierta=Tipo de cubierta
|
||||
presupuesto.tipo-cubierta-descripcion=Seleccione el tipo de cubierta y sus opciones
|
||||
presupuesto.tapa-blanda=Tapa blanda
|
||||
presupuesto.tapaBlanda=Tapa blanda
|
||||
presupuesto.tapa-dura=Tapa dura
|
||||
presupuesto.tapaDura=Tapa dura
|
||||
presupuesto.tapa-dura-lomo-redondo=Tapa dura lomo redondo
|
||||
presupuesto.tapaDuraLomoRedondo=Tapa dura lomo redondo
|
||||
presupuesto.sin-solapas=Sin solapas
|
||||
presupuesto.con-solapas=Con solapas
|
||||
presupuesto.impresion-cubierta=Impresión de cubierta
|
||||
|
||||
@ -6,5 +6,6 @@ validation.typeMismatchMsg=Tipo de dato no válido
|
||||
validation.patternMsg=El formato no es válido
|
||||
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.
|
||||
|
||||
|
||||
@ -0,0 +1,48 @@
|
||||
(() => {
|
||||
// si jQuery está cargado, añade CSRF a AJAX
|
||||
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
|
||||
const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.getAttribute('content');
|
||||
if (window.$ && csrfToken && csrfHeader) {
|
||||
$.ajaxSetup({
|
||||
beforeSend: function (xhr) {
|
||||
xhr.setRequestHeader(csrfHeader, csrfToken);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const language = document.documentElement.lang || 'es-ES';
|
||||
|
||||
// Comprueba dependencias antes de iniciar
|
||||
if (!window.DataTable) {
|
||||
console.error('DataTables no está cargado aún');
|
||||
return;
|
||||
}
|
||||
|
||||
new DataTable('#margenes-datatable', {
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
orderCellsTop: true,
|
||||
pageLength: 50,
|
||||
language: { url: '/assets/libs/datatables/i18n/' + language + '.json' },
|
||||
responsive: true,
|
||||
ajax: {
|
||||
url: '/configuracion/margenes-presupuestos/datatable',
|
||||
method: 'GET',
|
||||
data: function (d) {
|
||||
// filtros si los necesitas
|
||||
}
|
||||
},
|
||||
order: [[0, 'asc']],
|
||||
columns: [
|
||||
{ data: 'id', name: 'id', orderable: true },
|
||||
{ data: 'tipoEncuadernacion', name: 'tipoEncuadernacion', orderable: true },
|
||||
{ data: 'tipoCubierta', name: 'tipoCubierta', orderable: true },
|
||||
{ data: 'tiradaMin', name: 'tiradaMin', orderable: true },
|
||||
{ data: 'tiradaMax', name: 'tiradaMax', orderable: true },
|
||||
{ data: 'margenMax', name: 'margenMax', orderable: true },
|
||||
{ data: 'margenMin', name: 'margenMin', orderable: true },
|
||||
{ data: 'actions', name: 'actions' }
|
||||
],
|
||||
columnDefs: [{ targets: -1, orderable: false, searchable: false }]
|
||||
});
|
||||
})();
|
||||
@ -0,0 +1,108 @@
|
||||
<!doctype html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
layout:decorate="~{imprimelibros/layout}">
|
||||
|
||||
<head>
|
||||
<th:block layout:fragment="pagetitle" />
|
||||
<th:block th:replace="~{imprimelibros/partials/head-css :: head-css}" />
|
||||
<th:block layout:fragment="pagecss">
|
||||
<link th:href="@{/assets/css/presupuestador.css}" rel="stylesheet"
|
||||
th:unless="${#authorization.expression('isAuthenticated()')}" />
|
||||
<link th:href="@{/assets/libs/datatables/dataTables.bootstrap5.min.css}" rel="stylesheet" />
|
||||
</th:block>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div th:replace="~{imprimelibros/partials/topbar :: topbar}" />
|
||||
<div th:replace="~{imprimelibros/partials/sidebar :: sidebar}" />
|
||||
|
||||
<th:block layout:fragment="content">
|
||||
<div th:if="${#authorization.expression('isAuthenticated()')}">
|
||||
|
||||
<!-- Modales-->
|
||||
<div
|
||||
th:replace="imprimelibros/partials/modal-form :: modal('userFormModal', 'usuarios.add', 'modal-md', 'userModalBody')">
|
||||
</div>
|
||||
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/"><i class="ri-home-5-fill"></i></a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page" th:text="#{margenes-presupuesto.breadcrumb}">Márgenes de presupuesto</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid">
|
||||
|
||||
<button type="button" class="btn btn-secondary mb-3" id="addButton">
|
||||
<i class="ri-add-line align-bottom me-1"></i> <span th:text="#{margenes-presupuesto.add}">Añadir</span>
|
||||
</button>
|
||||
|
||||
<table id="margenes-datatable" class="table table-striped table-nowrap responsive w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" th:text="#{margenes-presupuesto.tabla.id}">ID</th>
|
||||
<th scope="col" th:text="#{margenes-presupuesto.tabla.tipo_encuadernacion}">Tipo encuadernación</th>
|
||||
<th scope="col" th:text="#{margenes-presupuesto.tabla.tipo_cubierta}">Tipo cubierta</th>
|
||||
<th scope="col" th:text="#{margenes-presupuesto.tabla.tirada_minima}">Tirada Mín.</th>
|
||||
<th scope="col" th:text="#{margenes-presupuesto.tabla.tirada_maxima}">Tirada Máx.</th>
|
||||
<th scope="col" th:text="#{margenes-presupuesto.tabla.margen_maximo}">Margen Máx.</th>
|
||||
<th scope="col" th:text="#{margenes-presupuesto.tabla.margen_minimo}">Margen Mín.</th>
|
||||
<th scope="col" th:text="#{margenes-presupuesto.tabla.acciones}">Acciones</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><input type="text" class="form-control form-control-sm margenes-presupuesto-filter" data-col="id" /></th>
|
||||
<th>
|
||||
<select class="form-select form-select-sm margenes-presupuesto-select-filter" id="search-encuadernacion">
|
||||
<option value="" th:text="#{margenes-presupuesto.todos}">Todos</option>
|
||||
<option value="fresado" th:text="#{presupuesto.fresado}">Fresado</option>
|
||||
<option value="cosido" th:text="#{presupuesto.cosido}">Cosido</option>
|
||||
<option value="espiral" th:text="#{presupuesto.espiral}">Espiral</option>
|
||||
<option value="wireo" th:text="#{presupuesto.wireo}">Wireo</option>
|
||||
<option value="grapado" th:text="#{presupuesto.grapado}">Grapado</option>
|
||||
</select>
|
||||
</th>
|
||||
<th>
|
||||
<select class="form-select form-select-sm margenes-presupuesto-select-filter" id="search-cubierta">
|
||||
<option value="" th:text="#{margenes-presupuesto.todos}">Todos</option>
|
||||
<option value="tapaBlanda" th:text="#{presupuesto.tapa-blanda}"></option>
|
||||
<option value="tapaDura" th:text="#{presupuesto.tapa-dura}"></option>
|
||||
<option value="tapaDuraLomoRedondo" th:text="#{presupuesto.tapa-dura-lomo-redondo}"></option>
|
||||
</select>
|
||||
</th>
|
||||
<th>
|
||||
<input type="text" class="form-control form-control-sm margenes-presupuesto-filter" data-col="tirada_min" />
|
||||
</th>
|
||||
<th>
|
||||
<input type="text" class="form-control form-control-sm margenes-presupuesto-filter" data-col="tirada_max" />
|
||||
</th>
|
||||
<th>
|
||||
<input type="text" class="form-control form-control-sm margenes-presupuesto-filter" data-col="margen_min" />
|
||||
</th>
|
||||
<th>
|
||||
<input type="text" class="form-control form-control-sm margenes-presupuesto-filter" data-col="margen_max" />
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</th:block>
|
||||
|
||||
<th:block layout:fragment="modal" />
|
||||
<th:block th:replace="~{theme/partials/vendor-scripts :: scripts}" />
|
||||
<th:block layout:fragment="pagejs">
|
||||
<script th:inline="javascript">
|
||||
window.languageBundle = /*[[${languageBundle}]]*/ {};
|
||||
</script>
|
||||
<script th:src="@{/assets/libs/datatables/datatables.min.js}"></script>
|
||||
<script th:src="@{/assets/libs/datatables/dataTables.bootstrap5.min.js}"></script>
|
||||
<script th:src="@{/assets/js/pages/imprimelibros/configuracion/margenes-presupuesto/list.js}"></script>
|
||||
</th:block>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -46,9 +46,15 @@
|
||||
</li>
|
||||
<div th:if="${#authentication.principal.role == 'SUPERADMIN'}">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link menu-link" href="/">
|
||||
<i class="ri-settings-2-line"></i> <span th:text="#{app.sidebar.configuracion}">Configuración</span>
|
||||
</a>
|
||||
<a class="nav-link menu-link collapsed" href="#sidebarConfig" data-bs-toggle="collapse" role="button" aria-expanded="false" aria-controls="sidebarConfig">
|
||||
<i class="ri-settings-2-line"></i> <span th:text="#{app.sidebar.configuracion}">Configuración</span>
|
||||
</a>
|
||||
<div class="collapse menu-dropdown" id="sidebarConfig">
|
||||
<ul class="nav nav-sm flex-column">
|
||||
<li class="nav-item">
|
||||
<a href="/configuracion/margenes-presupuestos" class="nav-link" th:text="#{margenes-presupuesto.titulo}">Márgenes de presupuesto</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</div>
|
||||
</ul>
|
||||
|
||||
Reference in New Issue
Block a user