mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-01-13 08:58:48 +00:00
falta presupuesto marcapaginas y maquetacion y revision general en admin. revisar user
This commit is contained in:
@ -7,12 +7,14 @@ import org.springframework.data.domain.*;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
import jakarta.persistence.criteria.*;
|
||||
import java.util.*;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class DataTable<T> {
|
||||
|
||||
/* ===== Tipos funcionales ===== */
|
||||
public interface FilterHook<T> extends BiConsumer<SpecBuilder<T>, DataTablesRequest> {
|
||||
}
|
||||
|
||||
@ -20,31 +22,55 @@ public class DataTable<T> {
|
||||
void add(Specification<T> extra);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filtro custom por campo virtual: te doy (root, query, cb, value) y me
|
||||
* devuelves un Predicate
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface FieldFilter<T> {
|
||||
Predicate apply(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb, String value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Orden custom por campo virtual: te doy (root, query, cb) y me devuelves la
|
||||
* Expression<?> para orderBy
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface FieldOrder<T> {
|
||||
Expression<?> apply(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);
|
||||
}
|
||||
|
||||
/* ===== Estado ===== */
|
||||
private final JpaSpecificationExecutor<T> repo;
|
||||
private final Class<T> entityClass;
|
||||
private final DataTablesRequest dt;
|
||||
private final List<String> searchable;
|
||||
|
||||
private final List<Function<T, Map<String, Object>>> adders = new ArrayList<>();
|
||||
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()
|
||||
.registerModule(new JavaTimeModule())
|
||||
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||
.registerModule(new JavaTimeModule())
|
||||
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||
|
||||
/** whitelist de campos ordenables “simples” (por nombre) */
|
||||
private List<String> orderable = null;
|
||||
|
||||
private boolean onlyAdded = false;
|
||||
public DataTable<T> onlyAddedColumns(){
|
||||
this.onlyAdded = true;
|
||||
return this;
|
||||
}
|
||||
/** mapas de comportamiento custom por campo */
|
||||
private final Map<String, FieldOrder<T>> orderCustom = new HashMap<>();
|
||||
private final Map<String, FieldFilter<T>> filterCustom = new HashMap<>();
|
||||
|
||||
private boolean onlyAdded = false;
|
||||
|
||||
/* ===== Ctor / factory ===== */
|
||||
private DataTable(JpaSpecificationExecutor<T> repo, Class<T> entityClass, DataTablesRequest dt,
|
||||
List<String> searchable) {
|
||||
this.repo = repo;
|
||||
this.entityClass = entityClass;
|
||||
this.dt = dt;
|
||||
this.searchable = searchable;
|
||||
this.searchable = searchable != null ? searchable : List.of();
|
||||
}
|
||||
|
||||
public static <T> DataTable<T> of(JpaSpecificationExecutor<T> repo, Class<T> entityClass, DataTablesRequest dt,
|
||||
@ -52,13 +78,19 @@ public class DataTable<T> {
|
||||
return new DataTable<>(repo, entityClass, dt, searchable);
|
||||
}
|
||||
|
||||
/** Equivalente a tu $q->where(...): establece condición base */
|
||||
/* ===== Fluent API ===== */
|
||||
public DataTable<T> onlyAddedColumns() {
|
||||
this.onlyAdded = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** WHERE base reusable */
|
||||
public DataTable<T> where(Specification<T> spec) {
|
||||
this.baseSpec = this.baseSpec.and(spec);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** add("campo", fn(entity)->valor|Map) */
|
||||
/** Campos renderizados */
|
||||
public DataTable<T> add(String field, Function<T, Object> fn) {
|
||||
adders.add(entity -> {
|
||||
Map<String, Object> m = new HashMap<>();
|
||||
@ -68,19 +100,19 @@ public class DataTable<T> {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* add(fn(entity)->Map<String,Object>) para devolver objetos anidados como tu
|
||||
* "logo"
|
||||
*/
|
||||
public DataTable<T> addIf(boolean condition, String field, Function<T, Object> fn) {
|
||||
if (condition)
|
||||
return add(field, fn);
|
||||
return this;
|
||||
}
|
||||
|
||||
public DataTable<T> add(Function<T, Map<String, Object>> fn) {
|
||||
adders.add(fn);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* edit("campo", fn(entity)->valor) sobreescribe un campo existente o lo crea si
|
||||
* no existe
|
||||
*/
|
||||
/** Edita/inyecta valor usando la entidad original (guardada como __entity) */
|
||||
@SuppressWarnings("unchecked")
|
||||
public DataTable<T> edit(String field, Function<T, Object> fn) {
|
||||
editors.add(row -> {
|
||||
row.put(field, fn.apply((T) row.get("__entity")));
|
||||
@ -89,73 +121,132 @@ public class DataTable<T> {
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Whitelist de campos simples ordenables (por nombre) */
|
||||
public DataTable<T> orderable(List<String> fields) {
|
||||
this.orderable = fields;
|
||||
return this;
|
||||
}
|
||||
|
||||
private List<String> getOrderable() {
|
||||
return (orderable == null || orderable.isEmpty()) ? this.searchable : this.orderable;
|
||||
/** Orden custom por campo virtual (expresiones) */
|
||||
public DataTable<T> orderable(String field, FieldOrder<T> orderFn) {
|
||||
this.orderCustom.put(field, orderFn);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** filter((builder, req) -> builder.add(miExtraSpec(req))) */
|
||||
/** Filtro custom por campo virtual (LIKE, rangos, etc.) */
|
||||
public DataTable<T> filter(String field, FieldFilter<T> filterFn) {
|
||||
this.filterCustom.put(field, filterFn);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Hook para añadir Specifications extra programáticamente */
|
||||
public DataTable<T> filter(FilterHook<T> hook) {
|
||||
filters.add(hook);
|
||||
return this;
|
||||
}
|
||||
|
||||
/* ===== Helpers ===== */
|
||||
private List<String> getOrderable() {
|
||||
return (orderable == null || orderable.isEmpty()) ? this.searchable : this.orderable;
|
||||
}
|
||||
|
||||
/* ===== Core ===== */
|
||||
public DataTablesResponse<Map<String, Object>> toJson(long totalCount) {
|
||||
// Construye spec con búsqueda global + base + filtros custom
|
||||
// 1) Spec base + búsqueda (global/columnas) + hooks programáticos
|
||||
Specification<T> spec = baseSpec.and(DataTablesSpecification.build(dt, searchable));
|
||||
final Specification<T>[] holder = new Specification[] { spec };
|
||||
filters.forEach(h -> h.accept(extra -> holder[0] = holder[0].and(extra), dt));
|
||||
spec = holder[0];
|
||||
|
||||
// Sort
|
||||
// Sort
|
||||
// Hooks externos
|
||||
filters.forEach(h -> h.accept(extra -> holder[0] = holder[0].and(extra), dt));
|
||||
|
||||
// 2) Filtros por columna “custom” (virtuales)
|
||||
for (var col : dt.columns) {
|
||||
if (col == null || !col.searchable)
|
||||
continue;
|
||||
if (col.name == null || col.name.isBlank())
|
||||
continue;
|
||||
if (!filterCustom.containsKey(col.name))
|
||||
continue;
|
||||
if (col.search == null || col.search.value == null || col.search.value.isBlank())
|
||||
continue;
|
||||
|
||||
var value = col.search.value;
|
||||
var filterFn = filterCustom.get(col.name);
|
||||
holder[0] = holder[0].and((root, query, cb) -> {
|
||||
Predicate p = filterFn.apply(root, query, cb, value);
|
||||
return p != null ? p : cb.conjunction();
|
||||
});
|
||||
}
|
||||
|
||||
// 3) Orden:
|
||||
// - Para campos “simples” (no custom): con Sort (Spring)
|
||||
// - Para campos “custom” (virtuales/expresiones): query.orderBy(...) dentro de
|
||||
// una spec
|
||||
Sort sort = Sort.unsorted();
|
||||
List<Sort.Order> simpleOrders = new ArrayList<>();
|
||||
boolean customApplied = false;
|
||||
|
||||
if (!dt.order.isEmpty() && !dt.columns.isEmpty()) {
|
||||
List<Sort.Order> orders = new ArrayList<>();
|
||||
for (var o : dt.order) {
|
||||
var col = dt.columns.get(o.column);
|
||||
String field = col != null ? col.name : null;
|
||||
if (col == null)
|
||||
continue;
|
||||
|
||||
String field = col.name;
|
||||
if (field == null || field.isBlank())
|
||||
continue;
|
||||
if (!col.orderable)
|
||||
continue;
|
||||
if (!getOrderable().contains(field))
|
||||
continue; // << usa tu whitelist
|
||||
continue;
|
||||
|
||||
orders.add(new Sort.Order(
|
||||
"desc".equalsIgnoreCase(o.dir) ? Sort.Direction.DESC : Sort.Direction.ASC,
|
||||
field));
|
||||
}
|
||||
if (!orders.isEmpty()) {
|
||||
sort = Sort.by(orders);
|
||||
} else {
|
||||
for (var c : dt.columns) {
|
||||
if (c != null && c.orderable && c.name != null && !c.name.isBlank()
|
||||
&& getOrderable().contains(c.name)) {
|
||||
sort = Sort.by(c.name);
|
||||
break;
|
||||
}
|
||||
if (orderCustom.containsKey(field)) {
|
||||
final boolean asc = !"desc".equalsIgnoreCase(o.dir);
|
||||
final FieldOrder<T> orderFn = orderCustom.get(field);
|
||||
|
||||
// aplica el ORDER BY custom dentro de la Specification (con Criteria)
|
||||
holder[0] = holder[0].and((root, query, cb) -> {
|
||||
Expression<?> expr = orderFn.apply(root, query, cb);
|
||||
if (expr != null) {
|
||||
query.orderBy(asc ? cb.asc(expr) : cb.desc(expr));
|
||||
}
|
||||
return cb.conjunction();
|
||||
});
|
||||
customApplied = true;
|
||||
} else {
|
||||
// orden simple por nombre de propiedad real
|
||||
simpleOrders.add(new Sort.Order(
|
||||
"desc".equalsIgnoreCase(o.dir) ? Sort.Direction.DESC : Sort.Direction.ASC,
|
||||
field));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Page
|
||||
if (!simpleOrders.isEmpty()) {
|
||||
sort = Sort.by(simpleOrders);
|
||||
}
|
||||
|
||||
// 4) Paginación (Sort para simples; custom order ya va dentro de la spec)
|
||||
int page = dt.length > 0 ? dt.start / dt.length : 0;
|
||||
Pageable pageable = dt.length > 0 ? PageRequest.of(page, dt.length, sort) : Pageable.unpaged();
|
||||
|
||||
var p = repo.findAll(holder[0], pageable);
|
||||
long filtered = p.getTotalElements();
|
||||
|
||||
// Mapear entidad -> Map base (via Jackson) + add/edit
|
||||
// 5) Mapeo a Map + add/edit
|
||||
List<Map<String, Object>> data = new ArrayList<>();
|
||||
for (T e : p.getContent()) {
|
||||
Map<String, Object> row = onlyAdded ? new HashMap<>() : om.convertValue(e, Map.class);
|
||||
row.put("__entity", e); // para editores que necesiten la entidad
|
||||
Map<String, Object> row;
|
||||
if (onlyAdded) {
|
||||
row = new HashMap<>();
|
||||
} else {
|
||||
try {
|
||||
row = om.convertValue(e, Map.class);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
row = new HashMap<>();
|
||||
}
|
||||
}
|
||||
row.put("__entity", e);
|
||||
for (var ad : adders)
|
||||
row.putAll(ad.apply(e));
|
||||
for (var ed : editors)
|
||||
@ -163,6 +254,12 @@ public class DataTable<T> {
|
||||
row.remove("__entity");
|
||||
data.add(row);
|
||||
}
|
||||
|
||||
return new DataTablesResponse<>(dt.draw, totalCount, filtered, data);
|
||||
}
|
||||
}
|
||||
|
||||
private Predicate nullSafePredicate(CriteriaBuilder cb) {
|
||||
// Devuelve conjunción para no interferir con los demás predicados
|
||||
return cb.conjunction();
|
||||
}
|
||||
}
|
||||
@ -23,9 +23,15 @@ public class DataTablesSpecification {
|
||||
DataTablesRequest.Column col = dt.columns.get(i);
|
||||
if (col.searchable && col.search != null && col.search.value != null && !col.search.value.isEmpty()) {
|
||||
try {
|
||||
ands.add(like(cb, root.get(col.name), col.search.value));
|
||||
Path<?> path = root;
|
||||
String[] parts = col.name.split("\\.");
|
||||
for (String part : parts) {
|
||||
path = path.get(part);
|
||||
}
|
||||
ands.add(like(cb, path, col.search.value));
|
||||
} catch (IllegalArgumentException ex) {
|
||||
// columna no mapeada o relación: la ignoramos
|
||||
System.out.println("[DT] columna no mapeada o relación: " + col.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,21 +1,22 @@
|
||||
package com.imprimelibros.erp.presupuesto;
|
||||
|
||||
import com.imprimelibros.erp.datatables.*;
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||
|
||||
import jakarta.persistence.criteria.Expression;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.persistence.criteria.JoinType;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.data.domain.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.text.NumberFormat;
|
||||
import java.time.*;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.imprimelibros.erp.datatables.*;
|
||||
import java.text.NumberFormat;
|
||||
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||
|
||||
@Service
|
||||
public class PresupuestoDatatableService {
|
||||
@ -28,301 +29,59 @@ public class PresupuestoDatatableService {
|
||||
this.repo = repo;
|
||||
}
|
||||
|
||||
/* ---------- API pública ---------- */
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public DataTablesResponse<Map<String, Object>> datatablePublicos(DataTablesRequest dt, Locale locale) {
|
||||
String term = extractSearch(dt);
|
||||
Pageable pageable = pageableFrom(dt);
|
||||
|
||||
EnumMatches matches = buildEnumMatches(term, locale);
|
||||
|
||||
Specification<Presupuesto> spec = baseSpec(term, matches, dt);
|
||||
|
||||
spec = spec.and((root, query, cb) -> cb.equal(root.get("origen"), "publico"));
|
||||
|
||||
Page<Presupuesto> page = repo.findAll(spec, pageable);
|
||||
|
||||
var rows = page.getContent().stream()
|
||||
.map(p -> mapPresupuestoPublico(p, locale))
|
||||
.toList();
|
||||
|
||||
return new DataTablesResponse<>(dt.draw, repo.count(), page.getTotalElements(), rows);
|
||||
return commonDataTable(dt, locale, "publico", true);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public DataTablesResponse<Map<String, Object>> datatablePrivados(DataTablesRequest dt, Locale locale) {
|
||||
String term = extractSearch(dt);
|
||||
Pageable pageable = pageableFrom(dt);
|
||||
|
||||
EnumMatches matches = buildEnumMatches(term, locale);
|
||||
|
||||
Specification<Presupuesto> spec = baseSpec(term, matches, dt);
|
||||
spec = spec.and((root, query, cb) -> cb.equal(root.get("origen"), "privado"));
|
||||
|
||||
Page<Presupuesto> page = repo.findAll(spec, pageable);
|
||||
|
||||
var rows = page.getContent().stream()
|
||||
.map(p -> mapPresupuestoPrivado(p, locale)) // 👈 otro mapper con más/otros campos
|
||||
.toList();
|
||||
|
||||
return new DataTablesResponse<>(dt.draw, repo.count(), page.getTotalElements(), rows);
|
||||
return commonDataTable(dt, locale, "privado", false);
|
||||
}
|
||||
|
||||
/* ---------- Helpers reutilizables ---------- */
|
||||
private DataTablesResponse<Map<String, Object>> commonDataTable(DataTablesRequest dt, Locale locale, String origen,
|
||||
boolean publico) {
|
||||
Long count = repo.findAllByOrigen(Presupuesto.Origen.valueOf(origen)).stream().count();
|
||||
|
||||
private String extractSearch(DataTablesRequest dt) {
|
||||
return (dt.search != null && dt.search.value != null) ? dt.search.value.trim().toLowerCase() : "";
|
||||
List<String> orderable = List.of(
|
||||
"id", "titulo", "user.fullName", "tipoEncuadernacion", "tipoCubierta", "tipoImpresion",
|
||||
"selectedTirada", "estado", "totalConIva", "paginas", "pais", "region", "ciudad", "updatedAt");
|
||||
|
||||
return DataTable.of(repo, Presupuesto.class, dt,
|
||||
List.of("")) // búsqueda global solo por campos simples
|
||||
.orderable(orderable)
|
||||
.where((root, query, cb) -> cb.equal(root.get("origen"), Presupuesto.Origen.valueOf(origen)))
|
||||
.onlyAddedColumns()
|
||||
.add("id", Presupuesto::getId)
|
||||
.add("titulo", Presupuesto::getTitulo)
|
||||
.add("tipoEncuadernacion", p -> msg(p.getTipoEncuadernacion().getMessageKey(), locale))
|
||||
.add("tipoCubierta", p -> msg(p.getTipoCubierta().getMessageKey(), locale))
|
||||
.add("tipoImpresion", p -> msg(p.getTipoImpresion().getMessageKey(), locale))
|
||||
.add("selectedTirada", Presupuesto::getSelectedTirada)
|
||||
.add("paginas", p -> n(p.getPaginasColor()) + n(p.getPaginasNegro()))
|
||||
.filter("paginas", (root, q, cb, value) -> {
|
||||
Expression<Integer> sum = cb.sum(
|
||||
cb.coalesce(root.get("paginasColor"), cb.literal(0)),
|
||||
cb.coalesce(root.get("paginasNegro"), cb.literal(0)));
|
||||
Expression<String> asStr = cb.function("CONCAT", String.class, cb.literal(""), sum);
|
||||
return cb.like(asStr, "%" + value.trim() + "%");
|
||||
})
|
||||
.orderable("paginas", (root, q, cb) -> cb.sum(
|
||||
cb.coalesce(root.get("paginasColor"), cb.literal(0)),
|
||||
cb.coalesce(root.get("paginasNegro"), cb.literal(0))))
|
||||
|
||||
.add("estado", p -> msg(p.getEstado().getMessageKey(), locale))
|
||||
.add("totalConIva", p -> formatCurrency(p.getTotalConIva(), locale))
|
||||
.addIf(publico, "pais", Presupuesto::getPais)
|
||||
.addIf(publico, "region", Presupuesto::getRegion)
|
||||
.addIf(publico, "ciudad", Presupuesto::getCiudad)
|
||||
.add("updatedAt", p -> formatDate(p.getUpdatedAt(), locale))
|
||||
.addIf(!publico, "user", p -> p.getUser() != null ? p.getUser().getFullName() : "")
|
||||
.add("actions", this::generarBotones)
|
||||
.toJson(count);
|
||||
}
|
||||
|
||||
private Map<String, String> extractColumnSearches(DataTablesRequest dt) {
|
||||
Map<String, String> byColumn = new HashMap<>();
|
||||
if (dt.columns == null)
|
||||
return byColumn;
|
||||
|
||||
for (var col : dt.columns) {
|
||||
// Importante: en el front usa columns[i][name] con el nombre del campo JPA
|
||||
String field = col.name;
|
||||
String value = (col.search != null && col.search.value != null)
|
||||
? col.search.value.trim()
|
||||
: "";
|
||||
if (field != null && !field.isBlank() && value != null && !value.isBlank()) {
|
||||
byColumn.put(field, value.toLowerCase());
|
||||
}
|
||||
}
|
||||
return byColumn;
|
||||
}
|
||||
|
||||
private Pageable pageableFrom(DataTablesRequest dt) {
|
||||
int page = dt.length > 0 ? dt.start / dt.length : 0;
|
||||
List<Sort.Order> orders = new ArrayList<>();
|
||||
for (var o : dt.order) {
|
||||
String field = dt.columns.get(o.column).name; // usa columns[i][name] en el front
|
||||
if (field == null || field.isBlank())
|
||||
continue;
|
||||
orders.add(
|
||||
new Sort.Order("desc".equalsIgnoreCase(o.dir) ? Sort.Direction.DESC : Sort.Direction.ASC, field));
|
||||
}
|
||||
Sort sort = orders.isEmpty() ? Sort.by(Sort.Order.desc("updatedAt")) : Sort.by(orders);
|
||||
return dt.length > 0 ? PageRequest.of(page, dt.length, sort) : Pageable.unpaged();
|
||||
}
|
||||
|
||||
private EnumMatches buildEnumMatches(String term, Locale locale) {
|
||||
Function<String, String> tr = key -> {
|
||||
try {
|
||||
return messageSource.getMessage(key, null, locale).toLowerCase();
|
||||
} catch (Exception e) {
|
||||
return key.toLowerCase();
|
||||
}
|
||||
};
|
||||
|
||||
var enc = Arrays.stream(Presupuesto.TipoEncuadernacion.values())
|
||||
.filter(e -> tr.apply(e.getMessageKey()).contains(term))
|
||||
.collect(() -> EnumSet.noneOf(Presupuesto.TipoEncuadernacion.class), EnumSet::add, EnumSet::addAll);
|
||||
|
||||
var cub = Arrays.stream(Presupuesto.TipoCubierta.values())
|
||||
.filter(e -> tr.apply(e.getMessageKey()).contains(term))
|
||||
.collect(() -> EnumSet.noneOf(Presupuesto.TipoCubierta.class), EnumSet::add, EnumSet::addAll);
|
||||
|
||||
var imp = Arrays.stream(Presupuesto.TipoImpresion.values())
|
||||
.filter(e -> tr.apply(e.getMessageKey()).contains(term))
|
||||
.collect(() -> EnumSet.noneOf(Presupuesto.TipoImpresion.class), EnumSet::add, EnumSet::addAll);
|
||||
|
||||
var est = Arrays.stream(Presupuesto.Estado.values())
|
||||
.filter(e -> tr.apply(e.getMessageKey()).contains(term))
|
||||
.collect(() -> EnumSet.noneOf(Presupuesto.Estado.class), EnumSet::add, EnumSet::addAll);
|
||||
|
||||
return new EnumMatches(enc, cub, imp, est);
|
||||
}
|
||||
|
||||
/**
|
||||
* WHERE + ORDER dinámico (paginas/estado) reutilizable para ambos datatables
|
||||
*/
|
||||
private Specification<Presupuesto> baseSpec(String term, EnumMatches m, DataTablesRequest dt) {
|
||||
return (root, query, cb) -> {
|
||||
List<Predicate> ors = new ArrayList<>();
|
||||
List<Predicate> ands = new ArrayList<>(); // filtros por columna (AND)
|
||||
|
||||
Expression<Integer> totalPag = cb.sum(
|
||||
cb.coalesce(root.get("paginasColor"), cb.literal(0)),
|
||||
cb.coalesce(root.get("paginasNegro"), cb.literal(0)));
|
||||
|
||||
if (!term.isBlank()) {
|
||||
String like = "%" + term + "%";
|
||||
ors.add(cb.like(cb.lower(root.get("titulo")), like));
|
||||
ors.add(cb.like(cb.lower(root.join("user").get("fullName")), like));
|
||||
ors.add(cb.like(cb.lower(root.get("ciudad")), like));
|
||||
ors.add(cb.like(cb.lower(root.get("region")), like));
|
||||
ors.add(cb.like(cb.lower(root.get("pais")), like));
|
||||
}
|
||||
if (!m.enc.isEmpty())
|
||||
ors.add(root.get("tipoEncuadernacion").in(m.enc));
|
||||
if (!m.cub.isEmpty())
|
||||
ors.add(root.get("tipoCubierta").in(m.cub));
|
||||
if (!m.imp.isEmpty())
|
||||
ors.add(root.get("tipoImpresion").in(m.imp));
|
||||
if (!m.est.isEmpty())
|
||||
ors.add(root.get("estado").in(m.est));
|
||||
|
||||
Map<String, String> byCol = extractColumnSearches(dt);
|
||||
for (var entry : byCol.entrySet()) {
|
||||
String field = entry.getKey();
|
||||
String value = entry.getValue();
|
||||
if (value.isBlank() || field.isBlank()
|
||||
|| field.contains("tipoEncuadernacion")
|
||||
|| field.contains("tipoCubierta")
|
||||
|| field.contains("tipoImpresion")
|
||||
|| field.contains("estado")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// --- CASO ESPECIAL: filtro por nombre del usuario ---
|
||||
if ("user".equals(field)) {
|
||||
var userJoin = root.join("user");
|
||||
var expr = cb.lower(userJoin.get("fullName"));
|
||||
ands.add(cb.like(expr, "%" + value.toLowerCase() + "%"));
|
||||
continue;
|
||||
}
|
||||
|
||||
// --- CASO ESPECIAL: filtro por total de páginas ---
|
||||
if ("paginas".equals(field)) {
|
||||
try {
|
||||
int paginas = Integer.parseInt(value);
|
||||
ands.add(cb.equal(totalPag, paginas));
|
||||
} catch (NumberFormatException nfe) {
|
||||
var asString = cb.function("CONCAT", String.class, cb.literal(""), totalPag);
|
||||
var safe = cb.function("COALESCE", String.class, asString, cb.literal(""));
|
||||
var strExpr = cb.lower(safe);
|
||||
ands.add(cb.like(strExpr, "%" + value.toLowerCase() + "%"));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// --- RESTO DE CAMPOS: acceso genérico ---
|
||||
var path = root.get(field);
|
||||
Class<?> type = path.getJavaType();
|
||||
|
||||
Expression<String> strExpr;
|
||||
if (String.class.isAssignableFrom(type)) {
|
||||
strExpr = cb.lower(path.as(String.class));
|
||||
} else {
|
||||
var asString = cb.function("CONCAT", String.class, cb.literal(""), path);
|
||||
var safe = cb.function("COALESCE", String.class, asString, cb.literal(""));
|
||||
strExpr = cb.lower(safe);
|
||||
}
|
||||
|
||||
ands.add(cb.like(strExpr, "%" + value.toLowerCase() + "%"));
|
||||
}
|
||||
|
||||
// ORDER BY especial si en columns[i][name] viene 'paginas' o 'estado'
|
||||
if (query != null && !query.getOrderList().isEmpty()) {
|
||||
var jpaOrders = new ArrayList<jakarta.persistence.criteria.Order>();
|
||||
for (var ob : query.getOrderList()) {
|
||||
String prop = ob.getExpression().toString();
|
||||
boolean asc = ob.isAscending();
|
||||
if ("paginas".equals(prop)) {
|
||||
var totalPagOrder = cb.sum(
|
||||
cb.coalesce(root.get("paginasColor"), cb.literal(0)),
|
||||
cb.coalesce(root.get("paginasNegro"), cb.literal(0)));
|
||||
jpaOrders.add(asc ? cb.asc(totalPagOrder) : cb.desc(totalPagOrder));
|
||||
} else if ("estado".equals(prop)) {
|
||||
var estadoStr = cb.function("str", String.class, root.get("estado"));
|
||||
jpaOrders.add(asc ? cb.asc(estadoStr) : cb.desc(estadoStr));
|
||||
} else {
|
||||
try {
|
||||
jpaOrders.add(asc ? cb.asc(root.get(prop)) : cb.desc(root.get(prop)));
|
||||
} catch (IllegalArgumentException e) {
|
||||
// El campo no existe (como 'paginas'), lo ignoramos
|
||||
// Opcional: puedes loggear si quieres
|
||||
// log.warn("Campo no encontrado para ORDER BY: {}", prop);
|
||||
}
|
||||
}
|
||||
}
|
||||
query.orderBy(jpaOrders);
|
||||
}
|
||||
|
||||
// === Compose final WHERE ===
|
||||
Predicate where = ors.isEmpty()
|
||||
? cb.conjunction()
|
||||
: cb.or(ors.toArray(new Predicate[0]));
|
||||
|
||||
if (!ands.isEmpty()) {
|
||||
where = cb.and(where, cb.and(ands.toArray(new Predicate[0])));
|
||||
}
|
||||
|
||||
return where;
|
||||
};
|
||||
}
|
||||
|
||||
/* ---------- Mappers de filas (puedes tener tantos como vistas) ---------- */
|
||||
|
||||
private Map<String, Object> mapPresupuestoPublico(Presupuesto p, Locale locale) {
|
||||
int paginas = n(p.getPaginasColor()) + n(p.getPaginasNegro());
|
||||
Map<String, Object> m = new HashMap<>();
|
||||
m.put("id", p.getId());
|
||||
m.put("titulo", p.getTitulo());
|
||||
m.put("tipoEncuadernacion", msg(p.getTipoEncuadernacion().getMessageKey(), locale));
|
||||
m.put("tipoCubierta", msg(p.getTipoCubierta().getMessageKey(), locale));
|
||||
m.put("tipoImpresion", msg(p.getTipoImpresion().getMessageKey(), locale));
|
||||
m.put("tirada", p.getSelectedTirada());
|
||||
m.put("paginas", paginas);
|
||||
m.put("estado", msg(p.getEstado().getMessageKey(), locale));
|
||||
m.put("totalConIva", formatCurrency(p.getTotalConIva(), locale));
|
||||
m.put("pais", p.getPais());
|
||||
m.put("region", p.getRegion());
|
||||
m.put("ciudad", p.getCiudad());
|
||||
m.put("updatedAt", formatDate(p.getUpdatedAt(), locale));
|
||||
if (p.getEstado().equals(Presupuesto.Estado.borrador)) {
|
||||
m.put("actions",
|
||||
"<div class=\"hstack gap-3 flex-wrap\">" +
|
||||
"<a href=\"javascript:void(0);\" data-id=\"" + p.getId()
|
||||
+ "\" class=\"link-success btn-edit-anonimo fs-15\"><i class=\"ri-eye-line\"></i></a>" +
|
||||
"<a href=\"javascript:void(0);\" data-id=\"" + p.getId()
|
||||
+ "\" class=\"link-danger btn-delete-anonimo fs-15\"><i class=\"ri-delete-bin-5-line\"></i></a>"
|
||||
+
|
||||
"</div>");
|
||||
} else {
|
||||
m.put("actions",
|
||||
"<div class=\"hstack gap-3 flex-wrap\">" +
|
||||
"<a href=\"javascript:void(0);\" data-id=\"" + p.getId()
|
||||
+ "\" class=\"link-success btn-edit-anonimo fs-15\"><i class=\"ri-eye-line\"></i></a>" +
|
||||
"</div>");
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
private Map<String, Object> mapPresupuestoPrivado(Presupuesto p, Locale locale) {
|
||||
int paginas = n(p.getPaginasColor()) + n(p.getPaginasNegro());
|
||||
Map<String, Object> m = new HashMap<>();
|
||||
m.put("id", p.getId());
|
||||
m.put("titulo", p.getTitulo());
|
||||
m.put("user", p.getUser().getFullName());
|
||||
m.put("tipoEncuadernacion", msg(p.getTipoEncuadernacion().getMessageKey(), locale));
|
||||
m.put("tipoCubierta", msg(p.getTipoCubierta().getMessageKey(), locale));
|
||||
m.put("tipoImpresion", msg(p.getTipoImpresion().getMessageKey(), locale));
|
||||
m.put("tirada", p.getSelectedTirada());
|
||||
m.put("paginas", paginas);
|
||||
m.put("estado", msg(p.getEstado().getMessageKey(), locale));
|
||||
m.put("totalConIva", formatCurrency(p.getTotalConIva(), locale));
|
||||
m.put("updatedAt", formatDate(p.getUpdatedAt(), locale));
|
||||
if (p.getEstado().equals(Presupuesto.Estado.borrador)) {
|
||||
m.put("actions",
|
||||
"<div class=\"hstack gap-3 flex-wrap\">" +
|
||||
"<a href=\"javascript:void(0);\" data-id=\"" + p.getId()
|
||||
+ "\" class=\"link-success btn-edit-privado fs-15\"><i class=\"ri-pencil-line\"></i></a>" +
|
||||
"<a href=\"javascript:void(0);\" data-id=\"" + p.getId()
|
||||
+ "\" class=\"link-danger btn-delete-privado fs-15\"><i class=\"ri-delete-bin-5-line\"></i></a>"
|
||||
+
|
||||
"</div>");
|
||||
} else {
|
||||
m.put("actions",
|
||||
"<div class=\"hstack gap-3 flex-wrap\">" +
|
||||
"<a href=\"javascript:void(0);\" data-id=\"" + p.getId()
|
||||
+ "\" class=\"link-success btn-edit-privado fs-15\"><i class=\"ri-pencil-line\"></i></a>" +
|
||||
"</div>");
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
/* ---------- utilidades ---------- */
|
||||
/* ---------- helpers ---------- */
|
||||
|
||||
private String msg(String key, Locale locale) {
|
||||
try {
|
||||
@ -349,15 +108,21 @@ public class PresupuestoDatatableService {
|
||||
private String formatCurrency(BigDecimal value, Locale locale) {
|
||||
if (value == null)
|
||||
return "";
|
||||
NumberFormat nf = NumberFormat.getCurrencyInstance(locale);
|
||||
return nf.format(value);
|
||||
return NumberFormat.getCurrencyInstance(locale).format(value);
|
||||
}
|
||||
|
||||
/* record para agrupar matches */
|
||||
private record EnumMatches(
|
||||
EnumSet<Presupuesto.TipoEncuadernacion> enc,
|
||||
EnumSet<Presupuesto.TipoCubierta> cub,
|
||||
EnumSet<Presupuesto.TipoImpresion> imp,
|
||||
EnumSet<Presupuesto.Estado> est) {
|
||||
private String generarBotones(Presupuesto p) {
|
||||
boolean borrador = p.getEstado() == Presupuesto.Estado.borrador;
|
||||
String id = String.valueOf(p.getId());
|
||||
String editBtn = "<a href=\"javascript:void(0);\" data-id=\"" + id + "\" class=\"link-success btn-edit-" +
|
||||
(p.getOrigen().equals(Presupuesto.Origen.publico) ? "anonimo" : "privado") + " fs-15\"><i class=\"ri-" +
|
||||
(p.getOrigen().equals(Presupuesto.Origen.publico) ? "eye" : "pencil") + "-line\"></i></a>";
|
||||
|
||||
String deleteBtn = borrador ? "<a href=\"javascript:void(0);\" data-id=\"" + id
|
||||
+ "\" class=\"link-danger btn-delete-"
|
||||
+ (p.getOrigen().equals(Presupuesto.Origen.publico) ? "anonimo" : "privado")
|
||||
+ " fs-15\"><i class=\"ri-delete-bin-5-line\"></i></a>" : "";
|
||||
|
||||
return "<div class=\"hstack gap-3 flex-wrap\">" + editBtn + deleteBtn + "</div>";
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,4 +25,6 @@ public interface PresupuestoRepository extends JpaRepository<Presupuesto, Long>,
|
||||
Optional<Presupuesto> findAnyById(@Param("id") Long id);
|
||||
|
||||
Optional<Presupuesto> findTopBySessionIdAndEstadoOrderByCreatedAtDesc(String sessionId, Presupuesto.Estado estado);
|
||||
|
||||
List<Presupuesto> findAllByOrigen(Presupuesto.Origen origen);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user