diff --git a/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoController.java b/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoController.java index 1e89a7e..321d936 100644 --- a/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoController.java +++ b/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoController.java @@ -2,23 +2,13 @@ package com.imprimelibros.erp.presupuesto; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; - -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; import java.util.HashMap; import java.util.Locale; import java.util.Map; -import java.util.Optional; -import java.util.TimeZone; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; import org.springframework.validation.BindingResult; @@ -28,13 +18,13 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.http.MediaType; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.core.type.TypeReference; + import com.imprimelibros.erp.datatables.*; import com.imprimelibros.erp.externalApi.skApiClient; import com.imprimelibros.erp.i18n.TranslationService; @@ -42,7 +32,6 @@ import com.imprimelibros.erp.presupuesto.classes.ImagenPresupuesto; import com.imprimelibros.erp.presupuesto.classes.PresupuestoMaquetacion; import com.imprimelibros.erp.presupuesto.classes.PresupuestoMarcapaginas; import com.imprimelibros.erp.presupuesto.dto.Presupuesto; -import com.imprimelibros.erp.presupuesto.dto.PresupuestoDTAnonimo; import com.imprimelibros.erp.presupuesto.validation.PresupuestoValidationGroups; import jakarta.servlet.http.HttpServletRequest; @@ -63,13 +52,13 @@ public class PresupuestoController { private final ObjectMapper objectMapper; private final TranslationService translationService; - private final PresupuestoRepository repo; + private final PresupuestoDatatableService dtService; public PresupuestoController(ObjectMapper objectMapper, TranslationService translationService, - PresupuestoRepository repo) { + PresupuestoDatatableService dtService) { this.objectMapper = objectMapper; this.translationService = translationService; - this.repo = repo; + this.dtService = dtService; } @PostMapping("/public/validar/datos-generales") @@ -444,85 +433,11 @@ public class PresupuestoController { @GetMapping(value = "/datatable/anonimos", produces = "application/json") @ResponseBody - public DataTablesResponse> datatable( - HttpServletRequest request, - Authentication authentication, - Locale locale) { + public DataTablesResponse> datatableAnonimos( + HttpServletRequest request, Authentication auth, Locale locale) { + DataTablesRequest dt = DataTablesParser.from(request); - - // Filtros (opcional): p.ej. estado desde select extra - String fEstado = Optional.ofNullable(dt.raw.get("f_estado")).orElse(""); // '', 'borrador', ... - String term = (dt.search != null && dt.search.value != null) ? dt.search.value.trim() : ""; - - // Pageable y Sort desde DataTables (usarás 'name' completos en el front) - int page = dt.length > 0 ? dt.start / dt.length : 0; - List orders = new ArrayList<>(); - for (var o : dt.order) { - String field = dt.columns.get(o.column).name; // EJ: "cliente.nombre", "tipoImpresion", "pais" - // Whitelist de campos ordenables: - switch (field) { - case "id", "titulo", - "tipoEncuadernacion", "tipoCubierta", "tipoImpresion", - "selectedTirada", "paginas", "estado", - "totalConIva", - "pais", "region", "ciudad", - "updatedAt" -> - orders.add(new Sort.Order( - "desc".equalsIgnoreCase(o.dir) ? Sort.Direction.DESC : Sort.Direction.ASC, - field)); - default -> { - /* ignora */ } - } - } - Sort sort = orders.isEmpty() ? Sort.by(Sort.Order.asc("id")) : Sort.by(orders); - Pageable pageable = dt.length > 0 ? PageRequest.of(page, dt.length, sort) : Pageable.unpaged(); - - // Query DTO - Page pageDTO = repo.datatableAnonimos(fEstado, term, pageable); - long total = repo.count(); - - // para formatear la fecha - ZoneId zone = null; - if (locale != null && locale.getCountry() != null && !locale.getCountry().isEmpty()) { - zone = TimeZone.getTimeZone(locale.toLanguageTag()).toZoneId(); - } else { - zone = ZoneId.systemDefault(); // fallback - } - DateTimeFormatter df = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm").withZone(zone); - - // Mapear DTO -> Map para DataTables + acciones - List> rows = pageDTO.getContent().stream().map(r -> { - Map m = new HashMap<>(); - m.put("id", r.id()); - m.put("titulo", r.titulo()); - m.put("tipoEncuadernacion", r.tipoEncuadernacion().name()); // o etiqueta i18n si quieres - m.put("tipoCubierta", r.tipoCubierta().name()); - m.put("tipoImpresion", r.tipoImpresion().name()); - m.put("tirada", r.selectedTirada()); - m.put("paginas", r.paginas()); - m.put("estado", r.estado().name()); - m.put("totalConIva", r.totalConIva()); - m.put("pais", r.pais()); - m.put("region", r.region()); - m.put("ciudad", r.ciudad()); - m.put("updatedAt", r.updatedAt() == null ? "" : df.format(r.updatedAt())); - - - // Mantén aquí tu “actions” como antes (puedes usar `tipo` en el css) - m.put("actions", - "
" + - " " + - " " + - "
"); - return m; - }).toList(); - - return new DataTablesResponse<>(dt.draw, total, pageDTO.getTotalElements(), rows); - + return dtService.datatableAnonimos(dt, locale); } } diff --git a/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoDatatableService.java b/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoDatatableService.java new file mode 100644 index 0000000..041cbee --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoDatatableService.java @@ -0,0 +1,233 @@ +package com.imprimelibros.erp.presupuesto; + +import jakarta.persistence.criteria.Predicate; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.domain.*; + +import java.math.BigDecimal; +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 { + + private final MessageSource messageSource; + private final PresupuestoRepository repo; + + public PresupuestoDatatableService(MessageSource messageSource, PresupuestoRepository repo) { + this.messageSource = messageSource; + this.repo = repo; + } + + /* ---------- API pública ---------- */ + + public DataTablesResponse> datatableAnonimos(DataTablesRequest dt, Locale locale) { + String term = extractSearch(dt); + Pageable pageable = pageableFrom(dt); + + EnumMatches matches = buildEnumMatches(term, locale); + + Specification spec = baseSpec(term, matches, dt); + Page page = repo.findAll(spec, pageable); + + var rows = page.getContent().stream() + .map(p -> mapAnonimoRow(p, locale)) // 👈 mapper específico “anonimos” + .toList(); + + return new DataTablesResponse<>(dt.draw, repo.count(), page.getTotalElements(), rows); + } + + public DataTablesResponse> datatableNoAnonimos(DataTablesRequest dt, Locale locale) { + String term = extractSearch(dt); + Pageable pageable = pageableFrom(dt); + + EnumMatches matches = buildEnumMatches(term, locale); + + Specification spec = baseSpec(term, matches, dt); + Page page = repo.findAll(spec, pageable); + + var rows = page.getContent().stream() + .map(p -> mapNoAnonimoRow(p, locale)) // 👈 otro mapper con más/otros campos + .toList(); + + return new DataTablesResponse<>(dt.draw, repo.count(), page.getTotalElements(), rows); + } + + /* ---------- Helpers reutilizables ---------- */ + + private String extractSearch(DataTablesRequest dt) { + return (dt.search != null && dt.search.value != null) ? dt.search.value.trim().toLowerCase() : ""; + } + + private Pageable pageableFrom(DataTablesRequest dt) { + int page = dt.length > 0 ? dt.start / dt.length : 0; + List 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 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 baseSpec(String term, EnumMatches m, DataTablesRequest dt) { + return (root, query, cb) -> { + List ors = new ArrayList<>(); + + if (!term.isBlank()) { + String like = "%" + term + "%"; + ors.add(cb.like(cb.lower(root.get("titulo")), 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)); + + // ORDER BY especial si en columns[i][name] viene 'paginas' o 'estado' + if (query != null && !query.getOrderList().isEmpty()) { + var jpaOrders = new ArrayList(); + for (var ob : query.getOrderList()) { + String prop = ob.getExpression().toString(); + boolean asc = ob.isAscending(); + if ("paginas".equals(prop)) { + var totalPag = cb.sum(cb.coalesce(root.get("paginasColor"), 0), + cb.coalesce(root.get("paginasNegro"), 0)); + jpaOrders.add(asc ? cb.asc(totalPag) : cb.desc(totalPag)); + } 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 { + jpaOrders.add(asc ? cb.asc(root.get(prop)) : cb.desc(root.get(prop))); + } + } + query.orderBy(jpaOrders); + } + + return ors.isEmpty() ? cb.conjunction() : cb.or(ors.toArray(new Predicate[0])); + }; + } + + /* ---------- Mappers de filas (puedes tener tantos como vistas) ---------- */ + + private Map mapAnonimoRow(Presupuesto p, Locale locale) { + int paginas = n(p.getPaginasColor()) + n(p.getPaginasNegro()); + Map 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)); + m.put("actions", + "
" + + "" + + "" + + + "
"); + return m; + } + + private Map mapNoAnonimoRow(Presupuesto p, Locale locale) { + Map m = mapAnonimoRow(p, locale); // base común + // añade/remueve campos específicos de “no anónimos” + // m.put("cliente", p.getCliente().getNombre()); // ejemplo + return m; + } + + /* ---------- utilidades ---------- */ + + private String msg(String key, Locale locale) { + try { + return messageSource.getMessage(key, null, locale); + } catch (Exception e) { + return key; + } + } + + private int n(Integer v) { + return v == null ? 0 : v; + } + + private String formatDate(Instant instant, Locale locale) { + if (instant == null) + return ""; + ZoneId zone = (locale != null && locale.getCountry() != null && !locale.getCountry().isEmpty()) + ? TimeZone.getTimeZone(locale.toLanguageTag()).toZoneId() + : ZoneId.systemDefault(); + var df = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm").withZone(zone); + return df.format(instant); + } + + private String formatCurrency(BigDecimal value, Locale locale) { + if (value == null) + return ""; + NumberFormat nf = NumberFormat.getCurrencyInstance(locale); + return nf.format(value); + } + + /* record para agrupar matches */ + private record EnumMatches( + EnumSet enc, + EnumSet cub, + EnumSet imp, + EnumSet est) { + } +} diff --git a/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoRepository.java b/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoRepository.java index e9fbf69..ade0dc5 100644 --- a/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoRepository.java +++ b/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoRepository.java @@ -5,60 +5,24 @@ import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; import com.imprimelibros.erp.presupuesto.dto.Presupuesto; -import com.imprimelibros.erp.presupuesto.dto.PresupuestoDTAnonimo; import java.util.*; @Repository public interface PresupuestoRepository extends JpaRepository, JpaSpecificationExecutor { - Optional findFirstBySessionIdAndOrigenAndEstadoInOrderByUpdatedAtDesc( - String sessionId, - Presupuesto.Origen origen, - Collection estados); + Optional findFirstBySessionIdAndOrigenAndEstadoInOrderByUpdatedAtDesc( + String sessionId, + Presupuesto.Origen origen, + Collection estados); - List findByOrigenAndEstado(Presupuesto.Origen origen, Presupuesto.Estado estado); + List findByOrigenAndEstado(Presupuesto.Origen origen, Presupuesto.Estado estado); - // Incluye borrados (ignora @Where) usando native - @Query(value = "SELECT * FROM presupuesto WHERE id = :id", nativeQuery = true) - Optional findAnyById(@Param("id") Long id); + // Incluye borrados (ignora @Where) usando native + @Query(value = "SELECT * FROM presupuesto WHERE id = :id", nativeQuery = true) + Optional findAnyById(@Param("id") Long id); - Optional findTopBySessionIdAndEstadoOrderByCreatedAtDesc(String sessionId, Presupuesto.Estado estado); - - @Query(""" - select new com.imprimelibros.erp.presupuesto.dto.PresupuestoDTAnonimo( - p.id, - p.titulo, - p.tipoEncuadernacion, - p.tipoCubierta, - p.tipoImpresion, - p.selectedTirada, - cast(coalesce(p.paginasColor,0) + coalesce(p.paginasNegro,0) as integer), - p.estado, - p.totalConIva, - p.pais, - p.region, - p.ciudad, - p.updatedAt - ) - from Presupuesto p - where - (:estado = '' or str(p.estado) = :estado) - and ( - :term = '' or - lower(p.titulo) like concat('%', lower(:term), '%') or - lower(p.ciudad) like concat('%', lower(:term), '%') or - lower(p.region) like concat('%', lower(:term), '%') or - lower(p.pais) like concat('%', lower(:term), '%') - ) - """) - Page datatableAnonimos( - @Param("estado") String estado, // '', 'borrador', 'aceptado', 'modificado' - @Param("term") String term, - Pageable pageable - ); + Optional findTopBySessionIdAndEstadoOrderByCreatedAtDesc(String sessionId, Presupuesto.Estado estado); } diff --git a/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoService.java b/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoService.java index 57e1f74..6a846ef 100644 --- a/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoService.java +++ b/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoService.java @@ -954,6 +954,8 @@ public class PresupuestoService { return price; } + + // ======================================================================= // Métodos privados // ======================================================================= @@ -1075,17 +1077,5 @@ public class PresupuestoService { return ip; } - private static String sha256Hex(String input) { - try { - var md = java.security.MessageDigest.getInstance("SHA-256"); - byte[] digest = md.digest(input.getBytes(java.nio.charset.StandardCharsets.UTF_8)); - StringBuilder sb = new StringBuilder(digest.length * 2); - for (byte b : digest) - sb.append(String.format("%02x", b)); - return sb.toString(); - } catch (Exception e) { - return null; - } - } - + } diff --git a/src/main/java/com/imprimelibros/erp/presupuesto/dto/Presupuesto.java b/src/main/java/com/imprimelibros/erp/presupuesto/dto/Presupuesto.java index 8faa4ef..1f1088f 100644 --- a/src/main/java/com/imprimelibros/erp/presupuesto/dto/Presupuesto.java +++ b/src/main/java/com/imprimelibros/erp/presupuesto/dto/Presupuesto.java @@ -92,7 +92,17 @@ public class Presupuesto extends AbstractAuditedEntity implements Cloneable { } public enum Estado { - borrador, aceptado, modificado + borrador("presupuesto.estado.borrador"), + aceptado("presupuesto.estado.aceptado"), + modificado("presupuesto.estado.modificado"); + + private final String messageKey; + Estado(String messageKey) { + this.messageKey = messageKey; + } + public String getMessageKey() { + return messageKey; + } } @Override diff --git a/src/main/java/com/imprimelibros/erp/presupuesto/dto/PresupuestoDTAnonimo.java b/src/main/java/com/imprimelibros/erp/presupuesto/dto/PresupuestoDTAnonimo.java deleted file mode 100644 index 832ead5..0000000 --- a/src/main/java/com/imprimelibros/erp/presupuesto/dto/PresupuestoDTAnonimo.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.imprimelibros.erp.presupuesto.dto; - -import java.math.BigDecimal; -import java.time.Instant; - -public record PresupuestoDTAnonimo( - Long id, - String titulo, // c.nombre - Presupuesto.TipoEncuadernacion tipoEncuadernacion, - Presupuesto.TipoCubierta tipoCubierta, - Presupuesto.TipoImpresion tipoImpresion, - Integer selectedTirada, - Integer paginas, - Presupuesto.Estado estado, - BigDecimal totalConIva, - String pais, - String region, - String ciudad, - Instant updatedAt -) {} diff --git a/src/main/resources/i18n/presupuesto_es.properties b/src/main/resources/i18n/presupuesto_es.properties index fd3ab82..651734a 100644 --- a/src/main/resources/i18n/presupuesto_es.properties +++ b/src/main/resources/i18n/presupuesto_es.properties @@ -12,6 +12,10 @@ presupuesto.add=Añadir presupuesto presupuesto.nav.presupuestos-cliente=Presupuestos cliente presupuesto.nav.presupuestos-anonimos=Presupuestos anónimos +presupuesto.estado.borrador=Borrador +presupuesto.estado.aceptado=Aceptado +presupuesto.estado.modificado=Modificado + presupuesto.tabla.id=ID presupuesto.tabla.titulo=Título presupuesto.tabla.cliente=Cliente diff --git a/src/main/resources/static/assets/js/pages/imprimelibros/presupuestos/list.js b/src/main/resources/static/assets/js/pages/imprimelibros/presupuestos/list.js index ca58cf5..36278af 100644 --- a/src/main/resources/static/assets/js/pages/imprimelibros/presupuestos/list.js +++ b/src/main/resources/static/assets/js/pages/imprimelibros/presupuestos/list.js @@ -24,9 +24,10 @@ orderCellsTop: true, stateSave: true, pageLength: 50, + lengthMenu: [10, 25, 50, 100, 500], language: { url: '/assets/libs/datatables/i18n/' + language + '.json' }, responsive: true, - dom: 'Bfrtip', + dom: 'Bflrtip', buttons: { dom: { button: { @@ -45,10 +46,6 @@ ajax: { url: '/presupuesto/datatable/anonimos', method: 'GET', - data: function (d) { - d.f_encuadernacion = $('#search-encuadernacion').val() || ''; // 'USER' | 'ADMIN' | 'SUPERADMIN' | '' - d.f_cubierta = $('#search-cubierta').val() || ''; // 'true' | 'false' | '' - } }, order: [[0, 'asc']], columns: [