direcciones terminadas

This commit is contained in:
2025-10-26 15:44:54 +01:00
parent 8e011e7fca
commit 8590235709
7 changed files with 643 additions and 43 deletions

View File

@ -1,5 +1,6 @@
package com.imprimelibros.erp.direcciones;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
@ -7,10 +8,14 @@ import java.util.Map;
import org.springframework.context.MessageSource;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
@ -25,6 +30,7 @@ import com.imprimelibros.erp.datatables.DataTablesRequest;
import com.imprimelibros.erp.datatables.DataTablesResponse;
import com.imprimelibros.erp.i18n.TranslationService;
import com.imprimelibros.erp.paises.PaisesService;
import com.imprimelibros.erp.users.User;
import com.imprimelibros.erp.users.UserDao;
import com.imprimelibros.erp.users.UserDetailsImpl;
@ -43,6 +49,7 @@ public class DireccionController {
protected final UserDao userRepo;
protected final TranslationService translationService;
public DireccionController(DireccionRepository repo, PaisesService paisesService,
MessageSource messageSource, UserDao userRepo, TranslationService translationService) {
this.repo = repo;
@ -60,19 +67,25 @@ public class DireccionController {
model.addAttribute("isUser", isUser ? 1 : 0);
List<String> keys = List.of(
"margenes-presupuesto.delete.title",
"margenes-presupuesto.delete.text",
"margenes-presupuesto.eliminar",
"margenes-presupuesto.delete.button",
"direcciones.delete.title",
"direcciones.delete.text",
"direcciones.eliminar",
"direcciones.delete.button",
"app.yes",
"app.cancelar",
"margenes-presupuesto.delete.ok.title",
"margenes-presupuesto.delete.ok.text");
"direcciones.delete.ok.title",
"direcciones.delete.ok.text",
"direcciones.btn.edit",
"direcciones.btn.delete",
"direcciones.telefono", "direcciones.isFacturacionShort");
Map<String, String> translations = translationService.getTranslations(locale, keys);
model.addAttribute("languageBundle", translations);
return "imprimelibros/direcciones/direccion-list";
if (isUser)
return "imprimelibros/direcciones/direccion-list-cliente";
else
return "imprimelibros/direcciones/direccion-list";
}
@GetMapping(value = "/datatable", produces = "application/json")
@ -172,12 +185,83 @@ public class DireccionController {
.toJson(total);
}
@GetMapping(value = "/datatableDirecciones", produces = "application/json")
@ResponseBody
public DataTablesResponse<Map<String, Object>> datatableCliente(
HttpServletRequest request,
Authentication authentication,
Locale locale) {
DataTablesRequest dt = DataTablesParser.from(request);
// Columnas visibles / lógicas para el DataTable en el frontend:
// id, cliente (nombre de usuario), alias, att, direccion, cp, ciudad,
// provincia, pais
List<String> searchable = List.of(
"id",
"alias",
"att", "direccion", "cp", "ciudad", "provincia", "pais", "telefono", "is_facturacion", "razonSocial",
"identificacionFiscal");
List<String> orderable = List.of(
"id",
"cliente", "alias",
"att", "direccion", "cp", "ciudad", "provincia", "pais", "telefono");
// Filtro base por rol (ROLE_USER solo ve sus direcciones)
Specification<Direccion> base = (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if (authentication != null && authentication.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_USER"))) {
String username = authentication.getName();
predicates.add(cb.equal(root.get("user").get("userName"), username));
}
return cb.and(predicates.toArray(new Predicate[0]));
};
long total = repo.count(base);
// Construcción del datatable con entity + spec
return DataTable
.of(repo, Direccion.class, dt, searchable)
.orderable(orderable)
// Columnas "crudas" (las que existen tal cual):
.edit("id", d -> d.getId())
.edit("alias", d -> d.getAlias())
.edit("att", d -> d.getAtt())
.edit("direccion", d -> d.getDireccion())
.edit("cp", d -> d.getCp())
.edit("ciudad", d -> d.getCiudad())
.edit("provincia", d -> d.getProvincia())
.edit("telefono", d -> d.getTelefono())
.edit("is_facturacion", d -> d.isDireccionFacturacion())
.edit("razon_social", d -> d.getRazonSocial())
.edit("tipo_identificacion_fiscal", d -> d.getTipoIdentificacionFiscal())
.edit("identificacion_fiscal", d -> d.getIdentificacionFiscal())
// pais = nombre localizado desde MessageSource usando el keyword del país
.add("pais", d -> {
// si tienes la relación read-only a Pais (d.getPais()) con .getKeyword()
String keyword = (d.getPais() != null) ? d.getPais().getKeyword() : null;
if (keyword == null || keyword.isBlank())
return d.getPaisCode3();
return messageSource.getMessage("paises." + keyword, null, keyword, locale);
})
// WHERE dinámico (spec base)
.where(base)
.toJson(total);
}
@GetMapping("form")
public String getForm(@RequestParam(required = false) Long id,
Direccion direccion,
BindingResult binding,
Model model,
HttpServletResponse response,
Authentication auth,
Locale locale) {
model.addAttribute("paises", paisesService.getForSelect("", "", locale).get("results"));
@ -196,7 +280,16 @@ public class DireccionController {
model.addAttribute("action", "/direcciones/" + id);
} else {
model.addAttribute("dirForm", new Direccion());
Direccion newDireccion = new Direccion();
boolean isUser = auth != null && auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_USER"));
if (isUser) {
User user = direccion.getUser() != null ? direccion.getUser() : null;
if (user != null) {
newDireccion.setUser(user);
}
}
model.addAttribute("dirForm", newDireccion);
model.addAttribute("action", "/direcciones");
}
return "imprimelibros/direcciones/direccion-form :: direccionForm";
@ -208,8 +301,17 @@ public class DireccionController {
BindingResult binding,
Model model,
HttpServletResponse response,
Authentication auth,
Locale locale) {
boolean isUser = auth != null && auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_USER"));
if (isUser) {
User current = userRepo.findByUserNameIgnoreCaseAndEnabledTrueAndDeletedFalse(auth.getName()).orElse(null);
direccion.setUser(current); // ignora lo que venga del hidden
}
if (binding.hasErrors()) {
response.setStatus(422);
model.addAttribute("paises", paisesService.getForSelect("", "", locale).get("results"));
@ -268,37 +370,51 @@ public class DireccionController {
return null;
}
/*
*
* @DeleteMapping("/{id}")
*
* @Transactional
* public ResponseEntity<?> delete(@PathVariable Long id, Authentication auth,
* Locale locale) {
*
* return repo.findById(id).map(u -> {
* try {
* u.setDeleted(true);
* u.setDeletedAt(LocalDateTime.now());
*
* repo.save(u); // ← NO delete(); guardamos el soft delete con deleted_by
* relleno
* return ResponseEntity.ok(Map.of("message",
* messageSource.getMessage("margenes-presupuesto.exito.eliminado", null,
* locale)));
* } catch (Exception ex) {
* return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
* .body(Map.of("message",
* messageSource.getMessage("margenes-presupuesto.error.delete-internal-error",
* null,
* locale)));
* }
* }).orElseGet(() -> ResponseEntity.status(HttpStatus.NOT_FOUND)
* .body(Map.of("message",
* messageSource.getMessage("margenes-presupuesto.error.not-found", null,
* locale))));
* }
*/
@DeleteMapping("/{id}")
@Transactional
public ResponseEntity<?> delete(@PathVariable Long id, Authentication auth, Locale locale) {
Direccion direccion = repo.findById(id).orElse(null);
if (direccion == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(Map.of("message", messageSource.getMessage("direcciones.error.noEncontrado", null, locale)));
}
boolean isUser = auth != null && auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_USER"));
Long ownerId = direccion.getUser() != null ? direccion.getUser().getId() : null;
Boolean isOwner = this.isOwnerOrAdmin(auth, ownerId);
if (isUser && !isOwner) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(Map.of("message",
messageSource.getMessage("direcciones.error.sinPermiso", null, locale)));
}
try {
direccion.setDeleted(true);
direccion.setDeletedAt(Instant.now());
if (auth != null && auth.getPrincipal() instanceof UserDetailsImpl udi) {
direccion.setDeletedBy(userRepo.getReferenceById(udi.getId()));
} else if (auth != null) {
userRepo.findByUserNameIgnoreCase(auth.getName()).ifPresent(direccion::setDeletedBy);
}
repo.saveAndFlush(direccion);
return ResponseEntity.ok(Map.of("message",
messageSource.getMessage("direcciones.exito.eliminado", null, locale)));
} catch (Exception ex) {
// Devuelve SIEMPRE algo en el catch
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("message",
messageSource.getMessage("direcciones.error.delete-internal-error", null, locale),
"detail",
ex.getClass().getSimpleName() + ": " + (ex.getMessage() != null ? ex.getMessage() : "")));
}
}
private boolean isOwnerOrAdmin(Authentication auth, Long ownerId) {
if (auth == null) {

View File

@ -1,5 +1,6 @@
direcciones.add=Añadir dirección
direcciones.editar=Editar dirección
direcciones.eliminar=Eliminar dirección
direcciones.breadcrumb=Direcciones
direcciones.add=Añadir dirección
@ -18,6 +19,7 @@ direcciones.pais=País
direcciones.instrucciones=Instrucciones de envío
direcciones.telefono=Teléfono
direcciones.isFacturacion=Usar también como dirección de facturación
direcciones.isFacturacionShort=Disponible para facturación
direcciones.razon_social=Razón Social
direcciones.tipo_identificacion_fiscal=Tipo de identificación fiscal
direcciones.identificacion_fiscal=Número de identificación fiscal
@ -39,9 +41,17 @@ direcciones.delete.text=¿Está seguro de que desea eliminar esta dirección?<br
direcciones.delete.ok.title=Dirección eliminada
direcciones.delete.ok.text=La dirección ha sido eliminada con éxito.
direcciones.btn.edit=Editar
direcciones.btn.delete=Eliminar
direcciones.buscar-placeholder=Buscar en direcciones...
direcciones.registros-pagina=Registros por página:
direcciones.exito.eliminado=Dirección eliminada con éxito.
direcciones.error.delete-internal-error=Error interno al eliminar la dirección.
direcciones.error.noEncontrado=Dirección no encontrada.
direcciones.error.sinPermiso=No tienes permiso para realizar esta acción.
direcciones.error.sinPermiso=No tiene permiso para realizar esta acción.
direcciones.form.error.required=Campo obligatorio.

View File

@ -0,0 +1,140 @@
// Requiere jQuery y Bootstrap 5 (para los data-bs-* de los modals)
export class DireccionCard {
/**
* @param {Object} opts
* @param {Object} opts.direccion // objeto con tus campos de BBDD
* - id, user_id, alias, att, direccion, cp, ciudad, provincia, pais_code3,
* telefono, is_facturacion, razon_social,
* tipo_identificacion_fiscal, identificacion_fiscal
*/
constructor(opts) {
this.opts = Object.assign({}, opts || {});
this.dir = this.opts.direccion || {};
this.id = `shippingAddress_${this.dir.id || Math.random().toString(36).slice(2, 8)}`;
this.$el = null;
}
// Escapa HTML para evitar XSS si llega texto “sucio”.
_esc(str) {
return String(str ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
// Construye la parte “línea de dirección”
_buildAddressLine() {
const p = this.dir;
const partes = [
this._esc(p.direccion),
[this._esc(p.cp), this._esc(p.ciudad)].filter(Boolean).join(" "),
[this._esc(p.provincia), this._esc((p.pais_code3 || "").toUpperCase())].filter(Boolean).join(", ")
].filter(Boolean);
return partes.join(", ");
}
toElement() {
if (this.$el) return this.$el;
const d = this.dir;
const isFact = !!d.is_facturacion || !!d.isFacturacion;
const header = this._esc(d.alias || "Dirección");
const contactLine = this._esc(d.att || "");
const razon_social = this._esc(d.razon_social || "");
const country = (d.pais || "").toUpperCase();
const addressLine = this._buildAddressLine();
const phoneLine = this._esc(d.telefono || "");
const extraFiscal = isFact
? `<span class="text-muted mt-2 fw-normal d-block">
<i class="ri-check-line"></i> ${window.languageBundle.get(['direcciones.isFacturacionShort']) || 'Disponible para facturación'}<br/>
${razon_social ? razon_social + "<br/>" : ""}
${this._esc(d.tipo_identificacion_fiscal || "")}${(d.tipo_identificacion_fiscal && d.identificacion_fiscal) ? ": " : ""}
${this._esc(d.identificacion_fiscal || "")}
</span>`
: "";
const html = `
<div class="col-lg-4 col-sm-6 ">
<div class="form-check card h-100 px-0">
<input
id="${this._esc(this.id)}"
type="hidden"
class="form-check-input"
data-id="${this._esc(d.id ?? '')}">
<label class="form-check-label h-100 d-flex flex-column" for="${this._esc(this.id)}">
<div class="p-2 mx-3">
<span class="mb-2 fw-semibold d-block text-muted text-uppercase">${header}</span>
${contactLine ? `<span class="fs-14 mb-1 d-block">${contactLine}</span>` : ''}
${addressLine ? `<span class="text-muted fw-normal text-wrap mb-1 d-block">${addressLine}</span>` : ''}
${country ? `<span class="text-muted fw-normal d-block">${country}</span>` : ''}
${phoneLine ? `<span class="text-muted fw-normal d-block">${window.languageBundle.get(['direcciones.telefono'])}: ${phoneLine}</span>` : ''}
${extraFiscal}
</div>
<!-- Acciones integradas en la tarjeta -->
<div class="d-flex flex-wrap align-items-center gap-2 px-2 py-1 bg-light rounded-bottom border-top mt-auto actions-row">
<a href="#" class="d-block text-body p-1 px-2 btn-edit-direccion" data-id="${this._esc(d.id ?? '')}">
<i class="ri-pencil-fill text-muted align-bottom me-1"></i> ${window.languageBundle.get(['direcciones.btn.edit']) || 'Editar'}
</a>
<a href="#" class="d-block text-body p-1 px-2 btn-delete-direccion" data-id="${this._esc(d.id ?? '')}">
<i class="ri-delete-bin-fill text-muted align-bottom me-1"></i> ${window.languageBundle.get(['direcciones.btn.delete']) || 'Eliminar'}
</a>
</div>
</label>
</div>
</div>
`;
this.$el = $(html);
return this.$el;
}
appendTo($container) {
const $node = this.toElement();
$container.append($node);
return this;
}
getValue() {
return this.dir.id ?? null;
}
}
/* ===================== Ejemplo de uso =====================
const direccion = {
id: 123,
user_id: 45,
alias: "Casa",
att: "Juan Pérez",
direccion: "C/ Hola 22",
cp: "28001",
ciudad: "Madrid",
provincia: "Madrid",
pais_code3: "ESP",
telefono: "600123123",
instrucciones: "Llamar al timbre 2ºB",
is_facturacion: true,
razon_social: "Editorial ImprimeLibros S.L.",
tipo_identificacion_fiscal: "CIF",
identificacion_fiscal: "B12345678"
};
new DireccionCard({
direccion,
name: "direccionSeleccionada",
checked: true,
editModal: "#direccionEditarModal",
removeModal: "#direccionEliminarModal",
onEdit: (dir) => { console.log("Editar", dir); },
onRemove: (dir) => { console.log("Eliminar", dir); },
onChange: (dir, checked) => { if (checked) console.log("Seleccionada", dir.id); }
}).appendTo($("#direccionesGrid"));
=========================================================== */

View File

@ -126,7 +126,6 @@
$(document).on('click', '.btn-edit-direccion', function (e) {
e.preventDefault();
const id = $(this).data('id');
e.preventDefault();
$.get('/direcciones/form', { id }, function (html) {
$('#direccionFormModalBody').html(html);
const title = $('#direccionFormModalBody #direccionForm').data('edit');
@ -169,7 +168,7 @@
confirmButton: 'btn btn-secondary w-xs mt-2',
},
});
$('#margenes-datatable').DataTable().ajax.reload(null, false);
$('#direccion-datatable').DataTable().ajax.reload(null, false);
},
error: function (xhr) {
// usa el mensaje del backend; fallback genérico por si no llega JSON

View File

@ -0,0 +1,219 @@
import { DireccionCard } from './direccionCard.js';
(() => {
// 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;
}
const $container = $('#direccionesContainer');
const $shadow = $('#dtDirecciones');
// Inicializa DataTable en modo server-side
const dt = $shadow.DataTable({
processing: true,
serverSide: true,
deferRender: true,
pageLength: 10, // sincronizado con el select
lengthChange: false, // manejado por #pageSize
searching: true,
ordering: true,
paging: true,
order: [[0, 'asc']], // orden inicial por alias
dom: 'tpi',
language: { url: '/assets/libs/datatables/i18n/' + language + '.json' },
ajax: {
url: '/direcciones/datatableDirecciones', // ajusta a tu endpoint
type: 'GET',
// Si tu backend espera JSON puro, descomenta:
// contentType: 'application/json',
// data: function (d) { return JSON.stringify(d); }
// Si espera form-urlencoded (el típico de DataTables), deja como está.
data: function (d) {
// Puedes incluir filtros extra aquí si los necesitas
// d.isFacturacionOnly = $('#chkFacturacion').prop('checked') ? 1 : 0;
}
},
// Mapea las columnas a las propiedades del JSON que retorna tu backend
columns: [
{ data: 'alias', name: 'alias' },
{ data: 'att', name: 'att' },
{ data: 'direccion', name: 'direccion' },
{ data: 'cp', name: 'cp' },
{ data: 'ciudad', name: 'ciudad' },
{ data: 'provincia', name: 'provincia' },
{ data: 'pais', name: 'pais' },
{ data: 'telefono', name: 'telefono' },
{ data: 'is_facturacion', name: 'is_facturacion' },
{ data: 'razon_social', name: 'razon_social' },
{ data: 'tipo_identificacion_fiscal', name: 'tipo_identificacion_fiscal' },
{ data: 'identificacion_fiscal', name: 'identificacion_fiscal' },
{ data: 'id', name: 'id' }
],
// No usamos rows/tds visibles; renderizamos tarjetas en drawCallback
drawCallback: function () {
const api = this.api();
const $container = $('#direccionesContainer').empty();
api.rows().every(function () {
const dir = this.data();
try {
new DireccionCard({
direccion: dir,
}).appendTo($container);
} catch (err) {
console.error('Error renderizando tarjeta de dirección', dir, err);
}
});
}
});
// Buscar
$('#buscadorDirecciones').on('keyup', function () {
dt.search(this.value).draw();
});
// Page size
$('#pageSize').on('change', function () {
dt.page.len(parseInt(this.value, 10)).draw();
});
const modalEl = document.getElementById('direccionFormModal');
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
$(document).on("change", ".direccionFacturacion", function () {
const isChecked = $(this).is(':checked');
if (isChecked) {
$('.direccionFacturacionItems').removeClass('d-none');
} else {
$('.direccionFacturacionItems').addClass('d-none');
$('#razonSocial').val('');
$('#tipoIdentificacionFiscal').val('DNI');
$('#identificacionFiscal').val('');
}
});
// Abrir "Crear"
$('#addButton').on('click', (e) => {
e.preventDefault();
$.get('/direcciones/form', function (html) {
$('#direccionFormModalBody').html(html);
const title = $('#direccionFormModalBody #direccionForm').data('add');
$('#direccionFormModal .modal-title').text(title);
modal.show();
});
});
// Abrir "Editar"
$(document).on('click', '.btn-edit-direccion', function (e) {
e.preventDefault();
const id = $(this).data('id');
$.get('/direcciones/form', { id }, function (html) {
$('#direccionFormModalBody').html(html);
const title = $('#direccionFormModalBody #direccionForm').data('edit');
$('#direccionFormModal .modal-title').text(title);
modal.show();
});
});
// Botón "Eliminar"
$(document).on('click', '.btn-delete-direccion', function (e) {
e.preventDefault();
const id = $(this).data('id');
Swal.fire({
title: window.languageBundle.get(['direcciones.delete.title']) || 'Eliminar dirección',
html: window.languageBundle.get(['direcciones.delete.text']) || 'Esta acción no se puede deshacer.',
icon: 'warning',
showCancelButton: true,
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-danger w-xs mt-2',
cancelButton: 'btn btn-light w-xs mt-2'
},
confirmButtonText: window.languageBundle.get(['direcciones.delete.button']) || 'Eliminar',
cancelButtonText: window.languageBundle.get(['app.cancelar']) || 'Cancelar',
}).then((result) => {
if (!result.isConfirmed) return;
$.ajax({
url: '/direcciones/' + id,
type: 'DELETE',
success: function () {
Swal.fire({
icon: 'success', title: window.languageBundle.get(['direcciones.delete.ok.title']) || 'Eliminado',
text: window.languageBundle.get(['direcciones.delete.ok.text']) || 'La dirección ha sido eliminada con éxito.',
showConfirmButton: true,
customClass: {
confirmButton: 'btn btn-secondary w-xs mt-2',
},
});
$('#dtDirecciones').DataTable().ajax.reload(null, false);
},
error: function (xhr) {
// usa el mensaje del backend; fallback genérico por si no llega JSON
const msg = (xhr.responseJSON && xhr.responseJSON.message)
|| 'Error al eliminar la direccion.';
Swal.fire({ icon: 'error', title: 'No se pudo eliminar', text: msg });
}
});
});
});
// Submit del form en el modal
$(document).on('submit', '#direccionForm', function (e) {
e.preventDefault();
const $form = $(this);
$.ajax({
url: $form.attr('action'),
type: 'POST', // PUT simulado via _method
data: $form.serialize(),
dataType: 'html',
success: function (html) {
// Si por cualquier motivo llega 200 con fragmento, lo insertamos igual
if (typeof html === 'string' && html.indexOf('id="direccionForm"') !== -1 && html.indexOf('<html') === -1) {
$('#direccionFormModalBody').html(html);
const isEdit = $('#direccionFormModalBody #direccionForm input[name="_method"][value="PUT"]').length > 0;
const title = $('#direccionFormModalBody #direccionForm').data(isEdit ? 'edit' : 'add');
$('#direccionModal .modal-title').text(title);
return;
}
// Éxito real: cerrar y recargar tabla
modal.hide();
dt.ajax.reload(null, false);
},
error: function (xhr) {
// Con 422 devolvemos el fragmento con errores aquí
if (xhr.status === 422 && xhr.responseText) {
$('#direccionFormModalBody').html(xhr.responseText);
const isEdit = $('#direccionFormModalBody #direccionForm input[name="_method"][value="PUT"]').length > 0;
const title = $('#direccionFormModalBody #direccionForm').data(isEdit ? 'edit' : 'add');
$('#direccionModal .modal-title').text(title);
return;
}
// Fallback
$('#direccionFormModalBody').html('<div class="p-3 text-danger">Error inesperado.</div>');
}
});
});
})();

View File

@ -18,7 +18,7 @@
<div class="invalid-feedback" th:if="${#fields.hasErrors('user.id')}" th:errors="*{user.id}"></div>
</div>
<input sec:authorize="hasAnyRole('USER')" type="hidden" th:field="*{user.id}" th:value="*{user.id}" />
<input sec:authorize="hasAnyRole('USER')" type="hidden" th:field="*{user.id}" />
<div class="form-group mt-2">
<label for="alias">

View File

@ -0,0 +1,116 @@
<!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('direccionFormModal', 'direcciones.add', 'modal-md', 'direccionFormModalBody')">
</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="#{direcciones.breadcrumb}">
Direcciones</li>
</ol>
</nav>
<div class="container-fluid">
<input type="hidden" id="isUser" th:value="${isUser}" />
<div class="d-flex">
<button type="button" class="btn btn-secondary mb-3" id="addButton">
<i class="ri-add-line align-bottom me-1"></i> <span th:text="#{direcciones.add}">Añadir</span>
</button>
</div>
<div class="d-flex mb-2 ms-0 ps-0">
<input type="text" id="buscadorDirecciones" class="form-control ms-0"
th:placeholder="#{direcciones.buscar-placeholder}"
style="width:auto; display:inline-block; margin-left:10px;" />
</div>
<div class="d-flex">
<label class="form-label mt-2 me-2" for="pageSize" th:text="#{direcciones.registros-pagina}">Registros
por página</label>
<select id="pageSize" class="form-select" style="width:auto">
<option value="10" selected>10</option>
<option value="20">20</option>
<option value="50">50</option>
</select>
</div>
<div id="direccionesContainer" class="row g-3 my-3"></div>
<!-- Tabla sombra para DataTables (oculta) -->
<table id="dtDirecciones" class="d-none" style="width:100%">
<thead>
<tr>
<th>alias</th>
<th>att</th>
<th>direccion</th>
<th>cp</th>
<th>ciudad</th>
<th>provincia</th>
<th>pais</th>
<th>telefono</th>
<th>is_facturacion</th>
<th>razon_social</th>
<th>tipo_identificacion_fiscal</th>
<th>identificacion_fiscal</th>
<th>id</th>
</tr>
</thead>
</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>
<!-- JS de Buttons y dependencias -->
<script th:src="@{/assets/libs/datatables/dataTables.buttons.min.js}"></script>
<script th:src="@{/assets/libs/jszip/jszip.min.js}"></script>
<script th:src="@{/assets/libs/pdfmake/pdfmake.min.js}"></script>
<script th:src="@{/assets/libs/pdfmake/vfs_fonts.min.js}"></script>
<script th:src="@{/assets/libs/datatables/buttons.html5.min.js}"></script>
<script th:src="@{/assets/libs/datatables/buttons.print.min.js}"></script>
<script th:src="@{/assets/libs/datatables/buttons.colVis.min.js}"></script>
<script th:if="${isUser==0}" type="module"
th:src="@{/assets/js/pages/imprimelibros/direcciones/list.js}"></script>
<script th:if="${isUser==1}" type="module"
th:src="@{/assets/js/pages/imprimelibros/direcciones/listc.js}"></script>
</th:block>
</body>
</html>