From 62dcff88697b618ddf7786ff3044ec93d8c53e6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Jim=C3=A9nez?= Date: Sat, 11 Oct 2025 18:16:55 +0200 Subject: [PATCH] falta ordenar por paginas y revisar la busqueda de los selects --- .../presupuesto/PresupuestoController.java | 40 +++- .../PresupuestoDatatableService.java | 179 +++++++++++++++--- .../service/PresupuestoService.java | 40 ++-- .../resources/i18n/presupuesto_es.properties | 1 + .../presupuestador/wizard-privado.js | 9 + .../imprimelibros/presupuestador/wizard.js | 11 +- .../pages/imprimelibros/presupuestos/list.js | 4 +- .../presupuestador-items/_buttons.html | 2 +- .../_datos-generales.html | 6 +- .../presupuestos/presupuesto-form.html | 16 +- .../presupuesto-list-items/tabla-cliente.html | 49 ++++- 11 files changed, 287 insertions(+), 70 deletions(-) create mode 100644 src/main/resources/static/assets/js/pages/imprimelibros/presupuestador/wizard-privado.js diff --git a/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoController.java b/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoController.java index 4c0c8ed..e44bf70 100644 --- a/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoController.java +++ b/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoController.java @@ -583,7 +583,9 @@ public class PresupuestoController { "presupuesto.plantilla-cubierta", "presupuesto.plantilla-cubierta-text", "presupuesto.impresion-cubierta", - "presupuesto.impresion-cubierta-help"); + "presupuesto.impresion-cubierta-help", + "presupuesto.exito.guardado", + "presupuesto.add.error.save.title"); Map translations = translationService.getTranslations(locale, keys); model.addAttribute("languageBundle", translations); @@ -592,10 +594,12 @@ public class PresupuestoController { model.addAttribute("ancho_alto_max", variableService.getValorEntero("ancho_alto_max")); model.addAttribute("appMode", "add"); + if (!mode.equals("public")) { model.addAttribute("cliente_id", clienteId); } + model.addAttribute("mode", mode); return "imprimelibros/presupuestos/presupuesto-form"; } @@ -617,15 +621,24 @@ public class PresupuestoController { } } - @GetMapping(value = "/datatable/anonimos", produces = "application/json") + @GetMapping(value = "/datatable/{tipo}", produces = "application/json") @ResponseBody - public DataTablesResponse> datatableAnonimos( - HttpServletRequest request, Authentication auth, Locale locale) { + public DataTablesResponse> datatable( + HttpServletRequest request, Authentication auth, Locale locale, + @PathVariable("tipo") String tipo) { DataTablesRequest dt = DataTablesParser.from(request); - return dtService.datatableAnonimos(dt, locale); + + if ("anonimos".equals(tipo)) { + return dtService.datatablePublicos(dt, locale); + } else if ("clientes".equals(tipo)) { + return dtService.datatablePrivados(dt, locale); + } else { + throw new IllegalArgumentException("Tipo de datatable no válido"); + } } + @DeleteMapping("/{id}") @Transactional public ResponseEntity delete(@PathVariable Long id, Authentication auth, Locale locale) { @@ -709,13 +722,26 @@ public class PresupuestoController { } try { + var resumen = presupuestoService.getTextosResumen(presupuesto, serviciosList, locale); + + Long cliente_id = objectMapper.convertValue(body.get("cliente_id"), Long.class); + if(id == null && cliente_id != null && !mode.equals("public")) { + + presupuesto.setUser(userRepo.findById(cliente_id).orElse(null)); + presupuesto.setOrigen(Presupuesto.Origen.privado); + } if (mode.equals("public")) { - var resumen = presupuestoService.getTextosResumen(presupuesto, serviciosList, locale); - presupuesto = presupuestoService.generateTotalizadores(presupuesto, serviciosList, resumen, locale); + presupuesto.setOrigen(Presupuesto.Origen.publico); + String sessionId = request.getSession(true).getId(); + String ip = request.getRemoteAddr(); + + presupuesto = presupuestoService.getDatosLocalizacion(presupuesto, sessionId, ip); if (id != null) { presupuesto.setId(id); // para que actualice, no cree uno nuevo } } + presupuesto = presupuestoService.generateTotalizadores(presupuesto, serviciosList, resumen, locale); + Map saveResult = presupuestoService.guardarPresupuesto(presupuesto); return ResponseEntity.ok(Map.of("id", saveResult.get("presupuesto_id"), "message", messageSource.getMessage("presupuesto.exito.guardado", null, locale))); diff --git a/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoDatatableService.java b/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoDatatableService.java index ea2205c..d0c89e9 100644 --- a/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoDatatableService.java +++ b/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoDatatableService.java @@ -1,5 +1,6 @@ package com.imprimelibros.erp.presupuesto; +import jakarta.persistence.criteria.Expression; import jakarta.persistence.criteria.Predicate; import org.springframework.data.jpa.domain.Specification; import org.springframework.data.domain.*; @@ -29,33 +30,38 @@ public class PresupuestoDatatableService { /* ---------- API pública ---------- */ - public DataTablesResponse> datatableAnonimos(DataTablesRequest dt, Locale locale) { + public DataTablesResponse> datatablePublicos(DataTablesRequest dt, Locale locale) { String term = extractSearch(dt); Pageable pageable = pageableFrom(dt); EnumMatches matches = buildEnumMatches(term, locale); Specification spec = baseSpec(term, matches, dt); + + spec = spec.and((root, query, cb) -> cb.equal(root.get("origen"), "publico")); + Page page = repo.findAll(spec, pageable); var rows = page.getContent().stream() - .map(p -> mapAnonimoRow(p, locale)) // 👈 mapper específico “anonimos” + .map(p -> mapPresupuestoPublico(p, locale)) .toList(); return new DataTablesResponse<>(dt.draw, repo.count(), page.getTotalElements(), rows); } - public DataTablesResponse> datatableNoAnonimos(DataTablesRequest dt, Locale locale) { + public DataTablesResponse> datatablePrivados(DataTablesRequest dt, Locale locale) { String term = extractSearch(dt); Pageable pageable = pageableFrom(dt); EnumMatches matches = buildEnumMatches(term, locale); Specification spec = baseSpec(term, matches, dt); + spec = spec.and((root, query, cb) -> cb.equal(root.get("origen"), "privado")); + Page page = repo.findAll(spec, pageable); var rows = page.getContent().stream() - .map(p -> mapNoAnonimoRow(p, locale)) // 👈 otro mapper con más/otros campos + .map(p -> mapPresupuestoPrivado(p, locale)) // 👈 otro mapper con más/otros campos .toList(); return new DataTablesResponse<>(dt.draw, repo.count(), page.getTotalElements(), rows); @@ -67,6 +73,24 @@ public class PresupuestoDatatableService { return (dt.search != null && dt.search.value != null) ? dt.search.value.trim().toLowerCase() : ""; } + private Map extractColumnSearches(DataTablesRequest dt) { + Map 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 orders = new ArrayList<>(); @@ -115,10 +139,16 @@ public class PresupuestoDatatableService { private Specification baseSpec(String term, EnumMatches m, DataTablesRequest dt) { return (root, query, cb) -> { List ors = new ArrayList<>(); + List ands = new ArrayList<>(); // filtros por columna (AND) + + Expression 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)); @@ -132,6 +162,56 @@ public class PresupuestoDatatableService { if (!m.est.isEmpty()) ors.add(root.get("estado").in(m.est)); + Map 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 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(); @@ -139,26 +219,42 @@ public class PresupuestoDatatableService { 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)); + 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 { - jpaOrders.add(asc ? cb.asc(root.get(prop)) : cb.desc(root.get(prop))); + 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); } - return ors.isEmpty() ? cb.conjunction() : cb.or(ors.toArray(new Predicate[0])); + // === 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 mapAnonimoRow(Presupuesto p, Locale locale) { + private Map mapPresupuestoPublico(Presupuesto p, Locale locale) { int paginas = n(p.getPaginasColor()) + n(p.getPaginasNegro()); Map m = new HashMap<>(); m.put("id", p.getId()); @@ -174,30 +270,55 @@ public class PresupuestoDatatableService { 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", - "
" + - "" + - "" - + - "
"); - } - else{ + if (p.getEstado().equals(Presupuesto.Estado.borrador)) { m.put("actions", - "
" + - "" + - "
"); + "
" + + "" + + "" + + + "
"); + } else { + 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 + private Map mapPresupuestoPrivado(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("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", + "
" + + "" + + "" + + + "
"); + } else { + m.put("actions", + "
" + + "" + + "
"); + } return m; } diff --git a/src/main/java/com/imprimelibros/erp/presupuesto/service/PresupuestoService.java b/src/main/java/com/imprimelibros/erp/presupuesto/service/PresupuestoService.java index 1b9df66..9ef7524 100644 --- a/src/main/java/com/imprimelibros/erp/presupuesto/service/PresupuestoService.java +++ b/src/main/java/com/imprimelibros/erp/presupuesto/service/PresupuestoService.java @@ -822,23 +822,9 @@ public class PresupuestoService { // 3) Enriquecer el Presupuesto a persistir presupuesto.setEstado(Presupuesto.Estado.borrador); if (mode.equals("public")) { - presupuesto.setOrigen(Presupuesto.Origen.publico); - presupuesto.setSessionId(sessionId); - // IP: guarda hash y trunc (si tienes campos). Si no, guarda tal cual en - // ip_trunc/ip_hash según tu modelo. - String ipTrunc = anonymizeIp(ip); - presupuesto.setIpTrunc(ipTrunc); - presupuesto.setIpHash(Integer.toHexString(ip.hashCode())); - // ubicación (si tienes un servicio GeoIP disponible; si no, omite estas tres - // líneas) - try { - GeoIpService.GeoData geo = geoIpService.lookup(ip).orElse(null); - presupuesto.setPais(geo.getPais()); - presupuesto.setRegion(geo.getRegion()); - presupuesto.setCiudad(geo.getCiudad()); - } catch (Exception ignore) { - } + presupuesto = getDatosLocalizacion(presupuesto, sessionId, ip); + } else presupuesto.setOrigen(Presupuesto.Origen.privado); @@ -878,6 +864,28 @@ public class PresupuestoService { return resumen; } + public Presupuesto getDatosLocalizacion(Presupuesto presupuesto, String sessionId, String ip) { + + presupuesto.setOrigen(Presupuesto.Origen.publico); + presupuesto.setSessionId(sessionId); + // IP: guarda hash y trunc (si tienes campos). Si no, guarda tal cual en + // ip_trunc/ip_hash según tu modelo. + String ipTrunc = anonymizeIp(ip); + presupuesto.setIpTrunc(ipTrunc); + presupuesto.setIpHash(Integer.toHexString(ip.hashCode())); + + // ubicación (si tienes un servicio GeoIP disponible; si no, omite estas tres + // líneas) + try { + GeoIpService.GeoData geo = geoIpService.lookup(ip).orElse(null); + presupuesto.setPais(geo.getPais()); + presupuesto.setRegion(geo.getRegion()); + presupuesto.setCiudad(geo.getCiudad()); + } catch (Exception ignore) { + } + return presupuesto; + } + public Presupuesto generateTotalizadores( Presupuesto presupuesto, List> servicios, diff --git a/src/main/resources/i18n/presupuesto_es.properties b/src/main/resources/i18n/presupuesto_es.properties index acb1968..f6d2736 100644 --- a/src/main/resources/i18n/presupuesto_es.properties +++ b/src/main/resources/i18n/presupuesto_es.properties @@ -288,6 +288,7 @@ presupuesto.add.cancel=Cancelar presupuesto.add.select-client=Seleccione cliente presupuesto.add.error.options=Debe seleccionar una opción presupuesto.add.error.options-client=Debe seleccionar un cliente +presupuesto.add.error.save.title=Error al guardar presupuesto.error.save-internal-error=No se puede guardar: error interno. presupuesto.exito.guardado=Presupuesto guardado con éxito. presupuesto.exito.guardado-actualizado=Presupuesto actualizado con éxito. diff --git a/src/main/resources/static/assets/js/pages/imprimelibros/presupuestador/wizard-privado.js b/src/main/resources/static/assets/js/pages/imprimelibros/presupuestador/wizard-privado.js new file mode 100644 index 0000000..a43824b --- /dev/null +++ b/src/main/resources/static/assets/js/pages/imprimelibros/presupuestador/wizard-privado.js @@ -0,0 +1,9 @@ +import PresupuestoWizard from './wizard.js'; + +const app = new PresupuestoWizard({ + mode: 'private', + readonly: false, + canSave: true, + useSessionCache: false, +}); +app.init(); \ No newline at end of file diff --git a/src/main/resources/static/assets/js/pages/imprimelibros/presupuestador/wizard.js b/src/main/resources/static/assets/js/pages/imprimelibros/presupuestador/wizard.js index 4c6af44..810a102 100644 --- a/src/main/resources/static/assets/js/pages/imprimelibros/presupuestador/wizard.js +++ b/src/main/resources/static/assets/js/pages/imprimelibros/presupuestador/wizard.js @@ -29,8 +29,8 @@ export default class PresupuestoWizard { ancho: 148, alto: 218, formatoPersonalizado: false, - paginasNegro: 0, - paginasColor: 32, + paginasNegro: 32, + paginasColor: 0, posicionPaginasColor: '', tipoEncuadernacion: 'fresado', }, @@ -245,7 +245,7 @@ export default class PresupuestoWizard { }); if (this.opts.canSave) { - $('#btn-guardar').on('click', async () => { + $('.guardar-presupuesto').on('click', async () => { const alert = $('#form-errors'); const servicios = []; @@ -263,6 +263,7 @@ export default class PresupuestoWizard { mode: this.opts.mode, presupuesto: this.#getPresupuestoData(), servicios: servicios, + cliente_id: $('#cliente_id').val() || null, }; try { alert.addClass('d-none').find('ul').empty(); @@ -274,7 +275,7 @@ export default class PresupuestoWizard { }).then((data) => { Swal.fire({ icon: 'success', - title: window.languageBundle?.get('common.guardado') || 'Guardado', + title: window.languageBundle?.get('presupuesto.exito.guardado') || 'Guardado', timer: 1800, buttonsStyling: false, customClass: { @@ -307,7 +308,7 @@ export default class PresupuestoWizard { } catch (e) { Swal.fire({ icon: 'error', - title: 'Error al guardar', + title: window.languageBundle?.get('presupuesto.add.error.save.title') || 'Error', text: e?.message || '', buttonsStyling: false, customClass: { 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 cdde956..25bcccd 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 @@ -138,9 +138,9 @@ import { preguntarTipoPresupuesto } from './presupuesto-utils.js'; if (!res) return; if (res.tipo === 'anonimo') { - console.log('Crear presupuesto ANÓNIMO'); + window.location.href = '/presupuesto/add/public'; } else { - console.log('Crear presupuesto de CLIENTE:', res.clienteId, res.clienteText); + window.location.href = '/presupuesto/add/private/' + res.clienteId; } }); diff --git a/src/main/resources/templates/imprimelibros/presupuestos/presupuestador-items/_buttons.html b/src/main/resources/templates/imprimelibros/presupuestos/presupuestador-items/_buttons.html index 0121c8a..242c9bd 100644 --- a/src/main/resources/templates/imprimelibros/presupuestos/presupuestador-items/_buttons.html +++ b/src/main/resources/templates/imprimelibros/presupuestos/presupuestador-items/_buttons.html @@ -3,7 +3,7 @@ th:if="${appMode == 'add' or appMode == 'edit'}" class="order-3 order-md-2 mx-md-auto d-flex"> diff --git a/src/main/resources/templates/imprimelibros/presupuestos/presupuestador-items/_datos-generales.html b/src/main/resources/templates/imprimelibros/presupuestos/presupuestador-items/_datos-generales.html index 21b6573..fa66d5e 100644 --- a/src/main/resources/templates/imprimelibros/presupuestos/presupuestador-items/_datos-generales.html +++ b/src/main/resources/templates/imprimelibros/presupuestos/presupuestador-items/_datos-generales.html @@ -132,7 +132,7 @@ + id="paginas-negro" name="paginas-negro" value="32">
Siempre deben ser pares
@@ -145,7 +145,7 @@ + id="paginas-color" name="paginas-color" value="0">
Siempre deben ser pares
@@ -253,8 +253,6 @@
-
-
@@ -59,7 +62,12 @@
- +
+ +
+
+ +
diff --git a/src/main/resources/templates/imprimelibros/presupuestos/presupuesto-list-items/tabla-cliente.html b/src/main/resources/templates/imprimelibros/presupuestos/presupuesto-list-items/tabla-cliente.html index bab43d9..67ef30d 100644 --- a/src/main/resources/templates/imprimelibros/presupuestos/presupuesto-list-items/tabla-cliente.html +++ b/src/main/resources/templates/imprimelibros/presupuestos/presupuesto-list-items/tabla-cliente.html @@ -1,5 +1,5 @@
- +
@@ -16,7 +16,52 @@ - + + + + + + + + + + + +
IDAcciones
+ + + + + + + + + +