implementado duplicar en la lista

This commit is contained in:
2025-11-29 23:30:22 +01:00
parent 58fd4815c6
commit c6e2322132
13 changed files with 5128 additions and 25 deletions

View File

@ -492,7 +492,8 @@ public class PresupuestoController {
String sessionId = request.getSession(true).getId();
String ip = IpUtils.getClientIp(request);
var resumen = presupuestoService.getResumen(p, serviciosList, datosMaquetacion, datosMarcapaginas, save, mode, locale, sessionId, ip);
var resumen = presupuestoService.getResumen(p, serviciosList, datosMaquetacion, datosMarcapaginas, save, mode,
locale, sessionId, ip);
return ResponseEntity.ok(resumen);
}
@ -519,7 +520,18 @@ public class PresupuestoController {
"presupuesto.add.cancel",
"presupuesto.add.select-client",
"presupuesto.add.error.options",
"presupuesto.add.error.options-client");
"presupuesto.add.error.options-client",
"presupuesto.duplicar.title",
"presupuesto.duplicar.text",
"presupuesto.duplicar.confirm",
"presupuesto.duplicar.cancelar",
"presupuesto.duplicar.aceptar",
"presupuesto.duplicar.required",
"presupuesto.duplicar.success.title",
"presupuesto.duplicar.success.text",
"presupuesto.duplicar.error.title",
"presupuesto.duplicar.error.internal"
);
Map<String, String> translations = translationService.getTranslations(locale, keys);
model.addAttribute("languageBundle", translations);
@ -562,15 +574,15 @@ public class PresupuestoController {
return "redirect:/presupuesto";
}
if(presupuestoOpt.get().getEstado() == Presupuesto.Estado.aceptado){
if (presupuestoOpt.get().getEstado() == Presupuesto.Estado.aceptado) {
Map<String, Object> resumen = presupuestoService.getTextosResumen(
presupuestoOpt.get(),
Utils.decodeJsonList(presupuestoOpt.get().getServiciosJson()),
Utils.decodeJsonMap(presupuestoOpt.get().getDatosMaquetacionJson()),
Utils.decodeJsonMap(presupuestoOpt.get().getDatosMarcapaginasJson()),
locale);
presupuestoOpt.get(),
Utils.decodeJsonList(presupuestoOpt.get().getServiciosJson()),
Utils.decodeJsonMap(presupuestoOpt.get().getDatosMaquetacionJson()),
Utils.decodeJsonMap(presupuestoOpt.get().getDatosMarcapaginasJson()),
locale);
model.addAttribute("resumen", resumen);
model.addAttribute("presupuesto", presupuestoOpt.get());
return "imprimelibros/presupuestos/presupuestador-view";
@ -595,6 +607,7 @@ public class PresupuestoController {
model.addAttribute("appMode", "edit");
}
model.addAttribute("id", presupuestoOpt.get().getId());
model.addAttribute("presupuesto", presupuestoOpt.get());
return "imprimelibros/presupuestos/presupuesto-form";
}
@ -780,4 +793,24 @@ public class PresupuestoController {
}
}
@PostMapping("/{id}/comentario")
@ResponseBody
public String actualizarComentario(@PathVariable Long id,
@RequestParam String comentario) {
presupuestoService.updateComentario(id, comentario);
return "OK";
}
@PostMapping("/duplicar/{id}")
@ResponseBody
public Map<String, Object> duplicarPresupuesto(
@PathVariable Long id,
@RequestParam(name = "titulo", defaultValue = "") String titulo) {
Long entity = presupuestoService.duplicarPresupuesto(id, titulo);
return Map.of("id", entity);
}
}

View File

@ -86,7 +86,7 @@ public class PresupuestoDatatableService {
.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)
.add("actions", p -> generarBotones(p, locale))
.where(base)
.toJson(count);
}
@ -115,18 +115,27 @@ public class PresupuestoDatatableService {
return df.format(instant);
}
private String generarBotones(Presupuesto p) {
private String generarBotones(Presupuesto p, Locale locale) {
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) || p.getEstado() == Presupuesto.Estado.aceptado ? "eye" : "pencil") + "-line\"></i></a>";
(p.getOrigen().equals(Presupuesto.Origen.publico) || p.getEstado() == Presupuesto.Estado.aceptado ? "eye" : "pencil") + "-line\" " +
"data-bs-toggle=\"tooltip\" data-bs-placement=\"top\" title=\"" +
msg(p.getEstado() == Presupuesto.Estado.aceptado ? "presupuesto.ver" : "presupuesto.editar", locale) + "\"></i></a>";
String duplicarBtn = !p.getOrigen().equals(Presupuesto.Origen.publico) ? "<a href=\"javascript:void(0);\" data-id=\"" + id
+ "\" class=\"link-success btn-duplicate-privado fs-15\"><i class=\"ri-file-copy-2-line\" data-bs-toggle=\"tooltip\" data-bs-placement=\"top\" title=\"" +
msg("presupuesto.duplicar", locale) + "\"></i></a>" : "";
String reimprimirBtn = p.getEstado() == Presupuesto.Estado.aceptado && !p.getOrigen().equals(Presupuesto.Origen.publico) ? "<a href=\"javascript:void(0);\" data-id=\"" + id
+ "\" class=\"link-success btn-reprint-privado fs-15\"><i class=\"ri-printer-line\" data-bs-toggle=\"tooltip\" data-bs-placement=\"top\" title=\"" +
msg("presupuesto.reimprimir", locale) + "\"></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>" : "";
+ " fs-15\"><i class=\"ri-delete-bin-5-line\" data-bs-toggle=\"tooltip\" data-bs-placement=\"top\" title=\"" +
msg("presupuesto.borrar", locale) + "\"></i></a>" : "";
return "<div class=\"hstack gap-3 flex-wrap\">" + editBtn + deleteBtn + "</div>";
return "<div class=\"hstack gap-3 flex-wrap\">" + editBtn + duplicarBtn + reimprimirBtn + deleteBtn + "</div>";
}
}

View File

@ -1361,6 +1361,29 @@ public class PresupuestoService {
return presupuestoRepository.findById(id).orElse(null);
}
public void updateComentario(Long presupuestoId, String comentario) {
Presupuesto presupuesto = presupuestoRepository.findById(presupuestoId).orElse(null);
if (presupuesto != null) {
presupuesto.setComentario(comentario);
presupuestoRepository.saveAndFlush(presupuesto);
}
}
public long duplicarPresupuesto(Long presupuestoId, String titulo) {
Presupuesto presupuesto = presupuestoRepository.findById(presupuestoId).orElse(null);
if (presupuesto != null) {
Presupuesto nuevo = presupuesto.clone();
nuevo.setId(null); // para que se genere uno nuevo
nuevo.setEstado(Presupuesto.Estado.borrador);
nuevo.setTitulo(titulo != null && !titulo.isEmpty() ? titulo : "[D] " + presupuesto.getTitulo());
nuevo.setIsReimpresion(false);
presupuestoRepository.saveAndFlush(nuevo);
return nuevo.getId();
}
return -1;
}
// =======================================================================
// Métodos privados
// =======================================================================

View File

@ -11,6 +11,11 @@ presupuesto.add-to-presupuesto=Añadir al presupuesto
presupuesto.calcular=Calcular
presupuesto.add=Añadir presupuesto
presupuesto.guardar=Guardar
presupuesto.duplicar=Duplicar
presupuesto.reimprimir=Reimprimir
presupuesto.editar=Editar
presupuesto.ver=Ver
presupuesto.borrar=Eliminar
presupuesto.add-to-cart=Añadir a la cesta
presupuesto.nav.presupuestos-cliente=Presupuestos cliente
@ -296,6 +301,18 @@ presupuesto.error.delete-permission-denied=No se puede eliminar: permiso denegad
presupuesto.error.delete-not-found=No se puede eliminar: presupuesto no encontrado.
presupuesto.error.delete-not-draft=Solo se pueden eliminar presupuestos en estado Borrador.
# Mensajes de duplicar presupuesto
presupuesto.duplicar.title=Duplicar presupuesto
presupuesto.duplicar.confirm=Si, DUPLICAR
presupuesto.duplicar.cancelar=Cancelar
presupuesto.duplicar.text=¿Está seguro de que desea duplicar este presupuesto?<br>Se creará una copia exacta del mismo en estado Borrador con el título introducido a continuación.
presupuesto.duplicar.required=El título es obligatorio.
presupuesto.duplicar.success.title=Presupuesto duplicado
presupuesto.duplicar.success.text=El presupuesto ha sido duplicado con éxito.
presupuesto.duplicar.aceptar=Aceptar.
presupuesto.duplicar.error.title=Error al duplicar presupuesto
presupuesto.duplicar.error.internal=No se puede duplicar: error interno.
# Añadir presupuesto
presupuesto.add.tipo=Tipo de presupuesto
presupuesto.add.anonimo=Anónimo

View File

@ -42,7 +42,41 @@ $(() => {
]
}
}
new Quill(item, snowEditorData);
var quill = new Quill(item, snowEditorData);
var initialContent = item.dataset.contenido || "";
// Contenido inicial desde Thymeleaf
var initialContent = item.dataset.contenido || "";
if (initialContent) {
if(initialContent.trim() !== "" && initialContent.trim() !== "<p><br></p>")
$('.badge-comentario').removeClass('d-none');
quill.clipboard.dangerouslyPasteHTML(initialContent);
}
quill.root.addEventListener("blur", function () {
let contenido = quill.root.innerHTML;
if(contenido.trim() !== "" && contenido.trim() !== "<p><br></p>"){
$('.badge-comentario').removeClass('d-none');
} else {
$('.badge-comentario').addClass('d-none');
}
if ($('#presupuesto_id').length > 0 && $('#presupuesto_id').val() !== "") {
$.ajax({
url: "/presupuesto/" + $('#presupuesto_id').val() + "/comentario",
method: "POST",
data: {
comentario: contenido
},
success: function () {
}
});
}
});
});
}
});

View File

@ -402,8 +402,6 @@ export default class PresupuestoWizard {
...this.#getInteriorData(),
...this.#getCubiertaData(),
selectedTirada: this.formData.selectedTirada
};
const sobrecubierta = data.sobrecubierta;

View File

@ -1,4 +1,4 @@
import { preguntarTipoPresupuesto } from './presupuesto-utils.js';
import { preguntarTipoPresupuesto, duplicar } from './presupuesto-utils.js';
(() => {
// si jQuery está cargado, añade CSRF a AJAX
@ -200,6 +200,17 @@ import { preguntarTipoPresupuesto } from './presupuesto-utils.js';
}
});
$('#presupuestos-clientes-datatable').on('click', '.btn-duplicate-privado', function (e) {
e.preventDefault();
const id = $(this).data('id');
let data = table_clientes.row($(this).parents('tr')).data();
const tituloOriginal = data.titulo;
duplicar(id, tituloOriginal);
});
$('#presupuestos-clientes-datatable').on('click', '.btn-delete-privado', function (e) {
e.preventDefault();

View File

@ -75,3 +75,94 @@ export async function preguntarTipoPresupuesto() {
}
}).then((r) => (r.isConfirmed ? r.value : null));
}
export function duplicar(id, titulo) {
// swal with input
Swal.fire({
title: window.languageBundle.get(['presupuesto.duplicar.title']) || 'Duplicar presupuesto',
html: window.languageBundle.get(['presupuesto.duplicar.text']) || `¿Deseas duplicar el presupuesto "${titulo}"?`,
icon: 'question',
input: 'text',
inputValue: `[D] ${titulo}`,
showCancelButton: true,
confirmButtonText: window.languageBundle.get(['presupuesto.duplicar.confirm']) || 'Sí, duplicar',
cancelButtonText: window.languageBundle.get(['presupuesto.duplicar.cancelar']) || 'Cancelar',
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
cancelButton: 'btn btn-light' // clases para cancelar
},
inputValidator: (value) => {
if (!value) {
return window.languageBundle.get(['presupuesto.duplicar.required']) || 'El título no puede estar vacío';
}
},
}).then((result) => {
if (result.isConfirmed) {
const tituloNuevo = result.value;
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);
}
});
}
$.ajax({
url: `presupuesto/duplicar/${id}`,
data: {
titulo: tituloNuevo,
},
method: 'POST',
success: function (response) {
if (response.id && response.id > 0) {
Swal.fire({
title: window.languageBundle.get(['presupuesto.duplicar.success.title']) || 'Presupuesto duplicado',
text: window.languageBundle.get(['presupuesto.duplicar.success.text']) || `El presupuesto "${titulo}" ha sido duplicado correctamente.`,
icon: 'success',
confirmButtonText: window.languageBundle.get(['presupuesto.duplicar.aceptar']) || 'Aceptar',
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-secondary' // clases para el botón confirmar
},
}).then(() => {
// Recargar la página o redirigir a la lista de presupuestos
window.location.href = '/presupuesto/edit/' + response.id;
});
} else {
Swal.fire({
title: window.languageBundle.get(['presupuesto.duplicar.error.title']) || 'Error al duplicar',
text: response.message || window.languageBundle.get(['presupuesto.duplicar.error.internal']) || 'Ha ocurrido un error al duplicar el presupuesto.',
icon: 'error',
confirmButtonText: window.languageBundle.get(['presupuesto.duplicar.aceptar']) || 'Aceptar',
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-secondary' // clases para el botón confirmar
},
});
}
},
error: function (xhr) {
Swal.fire({
title: window.languageBundle.get(['presupuesto.duplicar.error.title']) || 'Error al duplicar',
text: xhr.responseJSON?.message || window.languageBundle.get(['presupuesto.duplicar.error.internal']) || 'Ha ocurrido un error al duplicar el presupuesto.',
icon: 'error',
confirmButtonText: window.languageBundle.get(['presupuesto.duplicar.aceptar']) || 'Aceptar',
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-secondary' // clases para el botón confirmar
},
});
}
});
};
});
}

View File

@ -77,7 +77,9 @@
</thead>
<tbody>
<tr th:if="${resumen['linea0']}">
<td><img style="max-width: 60px; height: auto;" th:src="${resumen['imagen']}" th:alt="${resumen['imagen_alt']}" class="img-fluid" /></td>
<td><img style="max-width: 60px; height: auto;"
th:src="${resumen['imagen']}" th:alt="${resumen['imagen_alt']}"
class="img-fluid" /></td>
<td class="text-start" th:utext="${resumen['linea0'].descripcion}">
Descripción 1</td>
<td class="text-end" th:text="${resumen['linea0'].cantidad}">1</td>
@ -165,6 +167,38 @@
</button>
</div>
<div sec:authorize="isAuthenticated() and hasAnyRole('SUPERADMIN','ADMIN')">
<div class="accordion lefticon-accordion custom-accordionwithicon accordion-border-box mt-3"
id="accordionlefticon">
<div class="accordion-item material-shadow">
<h2 class="accordion-header" id="accordionComentario">
<button class="accordion-button collapsed" type="button"
data-bs-toggle="collapse"
data-bs-target="#accor_accordionComentario" aria-expanded="false"
aria-controls="accor_accordionComentario">
<span
th:text="#{presupuesto.comentario-administrador}">Comentario</span>
<span class="d-none badge badge-comentario bg-danger ms-1">!</span>
</button>
</h2>
<div id="accor_accordionComentario" class="accordion-collapse collapse"
aria-labelledby="accordionComentario"
data-bs-parent="#accordionlefticon">
<div class="accordion-body">
<div class="snow-editor" id="comentario" name="comentario"
th:attr="data-contenido=${presupuesto.comentario} "
style=" height: 300px;">
</div> <!-- end Snow-editor-->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -147,16 +147,19 @@
<h2 class="accordion-header" id="accordionComentario">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#accor_accordionComentario" aria-expanded="false"
aria-controls="accor_accordionComentario"
th:text="#{presupuesto.comentario-administrador}">Comentario
aria-controls="accor_accordionComentario">
<span th:text="#{presupuesto.comentario-administrador}">Comentario</span>
<span class="d-none badge badge-comentario bg-danger ms-1">!</span>
</button>
</h2>
<div id="accor_accordionComentario" class="accordion-collapse collapse"
aria-labelledby="accordionComentario" data-bs-parent="#accordionlefticon">
<div class="accordion-body">
<div class="snow-editor" name="comentario"
th:text="@{presupuesto.comentario}" style=" height: 300px;">
<div class="snow-editor" id="comentario" name="comentario"
th:attr="data-contenido=${presupuesto.comentario} "
style=" height: 300px;">
</div> <!-- end Snow-editor-->
</div>
</div>

View File

@ -12,7 +12,7 @@
<th scope="col" th:text="#{presupuesto.tabla.estado}">Estado</th>
<th scope="col" th:text="#{presupuesto.tabla.total-iva}">Total con IVA</th>
<th scope="col" th:text="#{presupuesto.tabla.updated-at}">Actualizado el</th>
<th scope="col" th:text="#{presupuesto.tabla.acciones}">Acciones</th>
<th style="min-width: 100px;" scope="col" th:text="#{presupuesto.tabla.acciones}">Acciones</th>
</tr>
<tr>
<th><input type="text" class="form-control form-control-sm presupuesto-filter" data-col="id" /></th>

View File

@ -13,7 +13,7 @@
<th scope="col" th:text="#{presupuesto.tabla.estado}">Estado</th>
<th scope="col" th:text="#{presupuesto.tabla.total-iva}">Total con IVA</th>
<th scope="col" th:text="#{presupuesto.tabla.updated-at}">Actualizado el</th>
<th scope="col" th:text="#{presupuesto.tabla.acciones}">Acciones</th>
<th style="min-width: 100px;" scope="col" th:text="#{presupuesto.tabla.acciones}">Acciones</th>
</tr>
<tr>
<th><input type="text" class="form-control form-control-sm presupuesto-filter" data-col="id" /></th>