falta borrar direcciones e implementar una vista de cliente

This commit is contained in:
2025-10-25 15:18:17 +02:00
parent 2ed032d7c6
commit 8e011e7fca
10 changed files with 292 additions and 226 deletions

View File

@ -1,5 +1,6 @@
package com.imprimelibros.erp.config;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
@ -10,13 +11,16 @@ import jakarta.validation.ValidatorFactory;
@Configuration
public class BeanValidationConfig {
// Asegura que usamos la factory de Spring (con SpringConstraintValidatorFactory)
// Usa el MessageSource (messages*.properties) para resolver {códigos}
@Bean
public LocalValidatorFactoryBean validator() {
return new LocalValidatorFactoryBean();
public LocalValidatorFactoryBean validator(MessageSource messageSource) {
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setValidationMessageSource(messageSource); // <-- CLAVE
return bean;
}
// Inserta esa factory en Hibernate/JPA
// Inserta esa factory en Hibernate/JPA (opcional pero correcto)
@Bean
public HibernatePropertiesCustomizer hibernateValidationCustomizer(ValidatorFactory vf) {
return props -> props.put("jakarta.persistence.validation.factory", vf);

View File

@ -10,6 +10,9 @@ import com.imprimelibros.erp.common.jpa.AbstractAuditedEntity;
import com.imprimelibros.erp.paises.Paises;
import com.imprimelibros.erp.users.User;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.NotBlank;
@Entity
@Table(name = "direcciones", indexes = {
@Index(name = "idx_direcciones_user", columnList = "user_id"),
@ -42,32 +45,41 @@ public class Direccion extends AbstractAuditedEntity implements Serializable {
private Long id;
// --- FK a users(id)
@NotNull(message = "{direcciones.form.error.required}")
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@NotBlank(message = "{direcciones.form.error.required}")
@Column(name = "alias", length = 100, nullable = false)
private String alias;
@NotBlank(message = "{direcciones.form.error.required}")
@Column(name = "att", length = 150, nullable = false)
private String att;
@NotBlank(message = "{direcciones.form.error.required}")
@Column(name = "direccion", length = 255, nullable = false)
private String direccion;
@NotNull(message = "{direcciones.form.error.required}")
@Column(name = "cp", length = 20, nullable = false)
private Integer cp;
@NotBlank(message = "{direcciones.form.error.required}")
@Column(name = "ciudad", length = 100, nullable = false)
private String ciudad;
@NotBlank(message = "{direcciones.form.error.required}")
@Column(name = "provincia", length = 100, nullable = false)
private String provincia;
// Usamos el code3 del país como FK lógica (String)
@NotBlank(message = "{direcciones.form.error.required}")
@Column(name = "pais_code3", length = 3, nullable = false)
private String paisCode3 = "esp";
@NotBlank(message = "{direcciones.form.error.required}")
@Column(name = "telefono", length = 30, nullable = false)
private String telefono;

View File

@ -12,6 +12,8 @@ import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
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.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@ -21,11 +23,15 @@ import com.imprimelibros.erp.datatables.DataTable;
import com.imprimelibros.erp.datatables.DataTablesParser;
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.UserDao;
import com.imprimelibros.erp.users.UserDetailsImpl;
import jakarta.persistence.criteria.Predicate;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
@Controller
@RequestMapping("/direcciones")
@ -34,11 +40,16 @@ public class DireccionController {
protected final DireccionRepository repo;
protected final PaisesService paisesService;
protected final MessageSource messageSource;
protected final UserDao userRepo;
protected final TranslationService translationService;
public DireccionController(DireccionRepository repo, PaisesService paisesService, MessageSource messageSource) {
public DireccionController(DireccionRepository repo, PaisesService paisesService,
MessageSource messageSource, UserDao userRepo, TranslationService translationService) {
this.repo = repo;
this.paisesService = paisesService;
this.messageSource = messageSource;
this.userRepo = userRepo;
this.translationService = translationService;
}
@GetMapping()
@ -48,6 +59,19 @@ public class DireccionController {
.anyMatch(a -> a.getAuthority().equals("ROLE_USER"));
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",
"app.yes",
"app.cancelar",
"margenes-presupuesto.delete.ok.title",
"margenes-presupuesto.delete.ok.text");
Map<String, String> translations = translationService.getTranslations(locale, keys);
model.addAttribute("languageBundle", translations);
return "imprimelibros/direcciones/direccion-list";
}
@ -80,13 +104,13 @@ public class DireccionController {
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));
predicates.add(cb.equal(root.get("user").get("userName"), username));
}
return cb.and(predicates.toArray(new Predicate[0]));
};
long total = repo.count(); // total sin filtro global
long total = repo.count(base);
// Construcción del datatable con entity + spec
return DataTable
@ -168,11 +192,11 @@ public class DireccionController {
return "imprimelibros/direcciones/direccion-form :: direccionForm";
}
model.addAttribute("direccion", opt.get());
model.addAttribute("dirForm", opt.get());
model.addAttribute("action", "/direcciones/" + id);
} else {
model.addAttribute("direccion", new Direccion());
model.addAttribute("dirForm", new Direccion());
model.addAttribute("action", "/direcciones");
}
return "imprimelibros/direcciones/direccion-form :: direccionForm";
@ -180,7 +204,7 @@ public class DireccionController {
@PostMapping
public String create(
Direccion direccion,
@Valid @ModelAttribute("dirForm") Direccion direccion,
BindingResult binding,
Model model,
HttpServletResponse response,
@ -188,130 +212,110 @@ public class DireccionController {
if (binding.hasErrors()) {
response.setStatus(422);
model.addAttribute("action", "/direcciones/");
model.addAttribute("paises", paisesService.getForSelect("", "", locale).get("results"));
model.addAttribute("action", "/direcciones");
model.addAttribute("dirForm", direccion);
return "imprimelibros/direcciones/direccion-form :: direccionForm";
}
var data = direccion;
try {
repo.save(data);
} catch (jakarta.validation.ConstraintViolationException vex) {
// Errores de Bean Validation disparados al flush (incluye tu @NoRangeOverlap)
vex.getConstraintViolations().forEach(v -> {
// intenta asignar al campo si existe, si no, error global
String path = v.getPropertyPath() != null ? v.getPropertyPath().toString() : null;
String code = v.getMessage() != null ? v.getMessage().trim() : "";
if (code.startsWith("{") && code.endsWith("}")) {
code = code.substring(1, code.length() - 1); // -> "validation.required"
}
if (path != null && binding.getFieldError(path) == null) {
binding.rejectValue(path, "validation", messageSource.getMessage(code, null, locale));
} else {
binding.reject("validation", messageSource.getMessage(code, null, locale));
}
});
response.setStatus(422);
model.addAttribute("action", "/direcciones/");
return "imprimelibros/direcciones/direccion-form :: direccionForm";
}
repo.save(data);
response.setStatus(201);
return null;
}
/*
@PutMapping("/{id}")
public String edit(
@PostMapping("/{id}")
public String update(
@PathVariable Long id,
MargenPresupuesto form,
@Valid @ModelAttribute("dirForm") Direccion direccion, // <- nombre distinto
BindingResult binding,
Model model,
Authentication auth,
HttpServletResponse response,
Locale locale) {
var uOpt = repo.findById(id);
if (uOpt.isEmpty()) {
binding.reject("usuarios.error.noEncontrado",
messageSource.getMessage("usuarios.error.noEncontrado", null, locale));
var opt = repo.findById(id);
if (opt.isEmpty()) {
binding.reject("direcciones.error.noEncontrado",
messageSource.getMessage("direcciones.error.noEncontrado", null, locale));
response.setStatus(404);
model.addAttribute("dirForm", direccion); // por si re-renderiza
model.addAttribute("action", "/direcciones/" + id);
return "imprimelibros/direcciones/direccion-form :: direccionForm";
}
Long ownerId = opt.get().getUser() != null ? opt.get().getUser().getId() : null;
if (!isOwnerOrAdmin(auth, ownerId)) {
binding.reject("direcciones.error.sinPermiso",
messageSource.getMessage("direcciones.error.sinPermiso", null, locale));
response.setStatus(403);
model.addAttribute("dirForm", direccion); // por si re-renderiza
model.addAttribute("action", "/direcciones/" + id);
return "imprimelibros/direcciones/direccion-form :: direccionForm";
}
if (binding.hasErrors()) {
response.setStatus(422);
model.addAttribute("action", "/configuracion/margenes-presupuesto/" + id);
return "imprimelibros/configuracion/margenes-presupuesto/margenes-presupuesto-form :: margenesPresupuestoForm";
model.addAttribute("dirForm", direccion); // <- importante
model.addAttribute("paises", paisesService.getForSelect("", "", locale).get("results"));
model.addAttribute("action", "/direcciones/" + id);
return "imprimelibros/direcciones/direccion-form :: direccionForm";
}
var entity = uOpt.get();
// 3) Copiar solamente campos editables
entity.setImporteMin(form.getImporteMin());
entity.setImporteMax(form.getImporteMax());
entity.setMargenMax(form.getMargenMax());
entity.setMargenMin(form.getMargenMin());
try {
repo.saveAndFlush(entity);
} catch (jakarta.validation.ConstraintViolationException vex) {
// Errores de Bean Validation disparados al flush (incluye tu @NoRangeOverlap)
vex.getConstraintViolations().forEach(v -> {
// intenta asignar al campo si existe, si no, error global
String path = v.getPropertyPath() != null ? v.getPropertyPath().toString() : null;
String code = v.getMessage() != null ? v.getMessage().trim() : "";
if (code.startsWith("{") && code.endsWith("}")) {
code = code.substring(1, code.length() - 1); // -> "validation.required"
}
if (path != null && binding.getFieldError(path) == null) {
binding.rejectValue(path, "validation", messageSource.getMessage(code, null, locale));
} else {
binding.reject("validation", messageSource.getMessage(code, null, locale));
}
});
response.setStatus(422);
model.addAttribute("action", "/configuracion/margenes-presupuesto/" + id);
return "imprimelibros/configuracion/margenes-presupuesto/margenes-presupuesto-form :: margenesPresupuestoForm";
} catch (org.springframework.dao.DataIntegrityViolationException dex) {
// Uniques, FKs, checks… mensajes de la BD
String msg = dex.getMostSpecificCause() != null ? dex.getMostSpecificCause().getMessage()
: dex.getMessage();
binding.reject("db.error", messageSource.getMessage(msg, null, locale));
response.setStatus(422);
model.addAttribute("action", "/configuracion/margenes-presupuesto/" + id);
return "imprimelibros/configuracion/margenes-presupuesto/margenes-presupuesto-form :: margenesPresupuestoForm";
}
response.setStatus(204);
repo.save(direccion);
response.setStatus(200);
return null;
}
@DeleteMapping("/{id}")
@Transactional
public ResponseEntity<?> delete(@PathVariable Long id, Authentication auth, Locale locale) {
/*
*
* @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))));
* }
*/
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))));
private boolean isOwnerOrAdmin(Authentication auth, Long ownerId) {
if (auth == null) {
return false;
}
boolean isAdmin = auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
if (isAdmin) {
return true;
}
// Aquí deberías obtener el ID del usuario actual desde tu servicio de usuarios
Long currentUserId = null;
if (auth != null && auth.getPrincipal() instanceof UserDetailsImpl udi) {
currentUserId = udi.getId();
} else if (auth != null) {
currentUserId = userRepo.findIdByUserNameIgnoreCase(auth.getName()).orElse(null);
}
return currentUserId != null && currentUserId.equals(ownerId);
}
*/
}

View File

@ -13,54 +13,22 @@ public interface DireccionRepository
extends JpaRepository<Direccion, Long>,
JpaSpecificationExecutor<Direccion> {
@Query("""
SELECT
d.id AS id,
d.alias AS alias,
d.att AS att,
d.direccion AS direccion,
d.cp AS cp,
d.ciudad AS ciudad,
d.provincia AS provincia,
d.paisCode3 AS paisCode3,
p.keyword AS paisKeyword,
d.telefono AS telefono,
d.direccionFacturacion AS direccionFacturacion,
d.razonSocial AS razonSocial,
d.tipoIdentificacionFiscal AS tipoIdentificacionFiscal,
d.identificacionFiscal AS identificacionFiscal,
u.fullName AS cliente
FROM Direccion d
JOIN d.user u
LEFT JOIN Paises p ON d.paisCode3 = p.code3
WHERE (:userId IS NULL OR u.id = :userId)
""")
select d from Direccion d
left join fetch d.user
left join fetch d.pais
where d.user.id = :id
""")
List<DireccionView> findAllWithPaisAndUser(@Param("userId") Long userId);
//findbyidwithPaisAndUser
@Query("""
SELECT
d.id AS id,
d.alias AS alias,
d.att AS att,
d.direccion AS direccion,
d.cp AS cp,
d.ciudad AS ciudad,
d.provincia AS provincia,
d.paisCode3 AS paisCode3,
p.keyword AS paisKeyword,
d.telefono AS telefono,
d.direccionFacturacion AS direccionFacturacion,
d.razonSocial AS razonSocial,
d.tipoIdentificacionFiscal AS tipoIdentificacionFiscal,
d.identificacionFiscal AS identificacionFiscal,
u.fullName AS cliente
FROM Direccion d
JOIN d.user u
LEFT JOIN Paises p ON d.paisCode3 = p.code3
WHERE (d.id = :id)
""")
Optional<DireccionView> findByIdWithPaisAndUser(@Param("id") Long id);
select d from Direccion d
left join fetch d.user
left join fetch d.pais
where d.id = :id
""")
Optional<Direccion> findByIdWithPaisAndUser(@Param("id") Long id);

View File

@ -2,13 +2,14 @@ package com.imprimelibros.erp.direcciones;
public interface DireccionView {
Long getId();
UserView getUser();
String getAlias();
String getAtt();
String getDireccion();
String getCp();
String getCiudad();
String getProvincia();
String getPaisCode3();
PaisView getPais();
String getPaisKeyword();
String getTelefono();
Boolean getIsFacturacion();
@ -16,4 +17,12 @@ public interface DireccionView {
String getTipoIdentificacionFiscal();
String getIdentificacionFiscal();
String getCliente();
interface UserView {
Long getId();
String getFullName();
}
interface PaisView {
String getCode3();
String getKeyword();
}
}

View File

@ -85,7 +85,6 @@ geoip.http.enabled=true
# Hibernate Timezone
#
spring.jpa.properties.hibernate.jdbc.time_zone=UTC
#
# PDF Templates
#

View File

@ -4,6 +4,8 @@ direcciones.editar=Editar dirección
direcciones.breadcrumb=Direcciones
direcciones.add=Añadir dirección
direcciones.edit=Editar dirección
direcciones.save=Guardar dirección
direcciones.user=Cliente
direcciones.alias=Alias
direcciones.alias-descripcion=Nombre descriptivo para identificar la dirección.
direcciones.nombre=Nombre y Apellidos
@ -21,6 +23,7 @@ direcciones.tipo_identificacion_fiscal=Tipo de identificación fiscal
direcciones.identificacion_fiscal=Número de identificación fiscal
direcciones.tabla.id=ID
direcciones.tabla.att=Att.
direcciones.tabla.cliente=Cliente
direcciones.tabla.acciones=Acciones
@ -30,5 +33,15 @@ direcciones.pasaporte=Pasaporte
direcciones.cif=C.I.F.
direcciones.vat_id=VAT ID
direcciones.error.noEncontrado=Dirección no encontrada.
direcciones.delete.title=Eliminar dirección
direcciones.delete.button=Si, ELIMINAR
direcciones.delete.text=¿Está seguro de que desea eliminar esta dirección?<br>Esta acción no se puede deshacer.
direcciones.delete.ok.title=Dirección eliminada
direcciones.delete.ok.text=La dirección ha sido eliminada con éxito.
direcciones.error.noEncontrado=Dirección no encontrada.
direcciones.error.sinPermiso=No tienes permiso para realizar esta acción.
direcciones.form.error.required=Campo obligatorio.

View File

@ -27,7 +27,7 @@
pageLength: 50,
language: { url: '/assets/libs/datatables/i18n/' + language + '.json' },
responsive: true,
dom: 'lrBtip',
dom: $('#isUser').val() == 1 ? 'lrtip' : 'lrBtip',
buttons: {
dom: {
button: {
@ -49,10 +49,10 @@
},
order: [[0, 'asc']],
columns: [
{ data: 'id', name: 'id', orderable: true, visible: $('#isUser').val() },
{ data: 'cliente', name: 'cliente', orderable: true },
{ data: 'id', name: 'id', orderable: true, visible: $('#isUser').val() == 1 ? false : true },
{ data: 'cliente', name: 'cliente', orderable: true, visible: $('#isUser').val() == 1 ? false : true },
{ data: 'alias', name: 'alias', orderable: true },
{ data: 'nombre', name: 'nombre', orderable: true },
{ data: 'att', name: 'att', orderable: true },
{ data: 'direccion', name: 'direccion', orderable: true },
{ data: 'cp', name: 'cp', orderable: true },
{ data: 'ciudad', name: 'ciudad', orderable: true },
@ -77,7 +77,7 @@
$(document).on("change", ".direccionFacturacion", function () {
const isChecked = $(this).is(':checked');
if(isChecked) {
if (isChecked) {
$('.direccionFacturacionItems').removeClass('d-none');
} else {
$('.direccionFacturacionItems').addClass('d-none');
@ -95,24 +95,50 @@
const title = $('#direccionFormModalBody #direccionForm').data('add');
$('#direccionFormModal .modal-title').text(title);
modal.show();
initSelect2Cliente(true);
});
});
function initSelect2Cliente(initialize = false) {
if ($('#isUser').val() == 0) {
const $sel = $('#user_id').select2({
dropdownParent: modalEl,
width: '100%',
ajax: {
url: 'users/api/get-users',
dataType: 'json',
delay: 250,
},
allowClear: true
});
if (initialize) {
const id = $sel.data('init-id');
const text = $sel.data('init-name');
const option = new Option(text, id, true, true);
$('#user_id').append(option).trigger('change');
}
}
}
// Abrir "Editar"
$(document).on('click', '.btn-edit-direccion', function (e) {
e.preventDefault();
const id = $(this).data('id');
/*$.get('/configuracion/margenes-presupuesto/form', { id }, function (html) {
$('#margenesPresupuestoModalBody').html(html);
const title = $('#margenesPresupuestoModalBody #margenesPresupuestoForm').data('edit');
$('#margenesPresupuestoModal .modal-title').text(title);
modal.show();*/
e.preventDefault();
$.get('/direcciones/form', { id }, function (html) {
$('#direccionFormModalBody').html(html);
const title = $('#direccionFormModalBody #direccionForm').data('edit');
$('#direccionFormModal .modal-title').text(title);
modal.show();
initSelect2Cliente(true);
});
});
// Botón "Eliminar"
$(document).on('click', '.btn-delete-margen', function (e) {
$(document).on('click', '.btn-delete-direccion', function (e) {
e.preventDefault();
const id = $(this).data('id');
@ -132,12 +158,12 @@
if (!result.isConfirmed) return;
$.ajax({
url: '/configuracion/margenes-presupuesto/' + id,
url: '/direcciones/' + id,
type: 'DELETE',
success: function () {
Swal.fire({
icon: 'success', title: window.languageBundle.get(['margenes-presupuesto.delete.ok.title']) || 'Eliminado',
text: window.languageBundle.get(['margenes-presupuesto.delete.ok.text']) || 'El margen ha sido eliminado con éxito.',
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',
@ -148,7 +174,7 @@
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 el usuario.';
|| 'Error al eliminar la direccion.';
Swal.fire({ icon: 'error', title: 'No se pudo eliminar', text: msg });
}
});
@ -156,7 +182,7 @@
});
// Submit del form en el modal
$(document).on('submit', '#margenesPresupuestoForm', function (e) {
$(document).on('submit', '#direccionForm', function (e) {
e.preventDefault();
const $form = $(this);
@ -167,11 +193,11 @@
dataType: 'html',
success: function (html) {
// Si por cualquier motivo llega 200 con fragmento, lo insertamos igual
if (typeof html === 'string' && html.indexOf('id="margenesPresupuestoForm"') !== -1 && html.indexOf('<html') === -1) {
$('#margenesPresupuestoModalBody').html(html);
const isEdit = $('#margenesPresupuestoModalBody #margenesPresupuestoForm input[name="_method"][value="PUT"]').length > 0;
const title = $('#margenesPresupuestoModalBody #margenesPresupuestoForm').data(isEdit ? 'edit' : 'add');
$('#margenesPresupuestoModal .modal-title').text(title);
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
@ -181,14 +207,15 @@
error: function (xhr) {
// Con 422 devolvemos el fragmento con errores aquí
if (xhr.status === 422 && xhr.responseText) {
$('#margenesPresupuestoModalBody').html(xhr.responseText);
const isEdit = $('#margenesPresupuestoModalBody #margenesPresupuestoForm input[name="_method"][value="PUT"]').length > 0;
const title = $('#margenesPresupuestoModalBody #margenesPresupuestoForm').data(isEdit ? 'edit' : 'add');
$('#margenesPresupuestoModal .modal-title').text(title);
$('#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);
initSelect2Cliente(true);
return;
}
// Fallback
$('#margenesPresupuestoModalBody').html('<div class="p-3 text-danger">Error inesperado.</div>');
$('#direccionFormModalBody').html('<div class="p-3 text-danger">Error inesperado.</div>');
}
});
});

View File

@ -1,44 +1,66 @@
<div th:fragment="direccionForm">
<form id="direccionForm" novalidate th:action="${action}" th:object="${direccion}" method="post"
<form id="direccionForm" novalidate th:action="${action}" th:object="${dirForm}" method="post"
th:data-add="#{direcciones.add}" th:data-edit="#{direcciones.editar}">
<div class="form-group">
<div class="alert alert-danger" th:if="${#fields.hasGlobalErrors()}" th:each="err : ${#fields.globalErrors()}">
<span th:text="${err}">Error</span>
</div>
<div sec:authorize="hasAnyRole('SUPERADMIN','ADMIN')" class="form-group">
<label for="user_id">
<span th:text="#{direcciones.user}">Cliente</span>
<span class="text-danger">*</span>
</label>
<select class="form-control select2 direccion-item" id="user_id" th:field="*{user}" th:attr="data-init-id=*{user?.id},
data-init-name=*{user?.fullName}" th:classappend="${#fields.hasErrors('user')} ? ' is-invalid'">
</select>
<div class="invalid-feedback" th:if="${#fields.hasErrors('user')}" th:errors="*{user}"></div>
<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}" />
<div class="form-group mt-2">
<label for="alias">
<span th:text="#{direcciones.alias}">Alias</span>
<span class="text-danger">*</span>
</label>
<input class="form-control direccion-item" id="alias" th:field="*{alias}" maxlength="100" required>
<div class="invalid-feedback"></div>
<input class="form-control direccion-item" id="alias" th:field="*{alias}" maxlength="100" required
th:classappend="${#fields.hasErrors('alias')} ? ' is-invalid'">
<div class="invalid-feedback" th:if="${#fields.hasErrors('alias')}" th:errors="*{alias}"></div>
<label th:text="#{direcciones.alias-descripcion}" class="form-text text-muted"></label>
</div>
<div class="form-group">
<div class="form-group mt-2">
<label for="att">
<span th:text="#{direcciones.nombre}">Nombre y Apellidos</span>
<span class="text-danger">*</span>
</label>
<input class="form-control direccion-item" id="att" th:field="*{att}" maxlength="150" required>
<div class="invalid-feedback"></div>
<input class="form-control direccion-item" id="att" th:field="*{att}" maxlength="150" required
th:classappend="${#fields.hasErrors('att')} ? ' is-invalid'">
<div class="invalid-feedback" th:if="${#fields.hasErrors('att')}" th:errors="*{att}"></div>
</div>
<div class="form-group">
<div class="form-group mt-2">
<label for="direccion">
<span th:text="#{direcciones.direccion}">Dirección</span>
<span class="text-danger">*</span>
</label>
<textarea class="form-control direccion-item" id="direccion" th:field="*{direccion}" maxlength="255"
required style="max-height: 125px;"></textarea>
<div class="invalid-feedback"></div>
required style="max-height: 125px;"
th:classappend="${#fields.hasErrors('direccion')} ? ' is-invalid'"></textarea>
<div class="invalid-feedback" th:if="${#fields.hasErrors('direccion')}" th:errors="*{direccion}"></div>
</div>
<div class="row">
<div class="row mt-2">
<div class="form-group col-lg-6 col-md-6 col-sm-12 ml-0">
<label for="cp">
<span th:text="#{direcciones.cp}">Código Postal</span>
<span class="text-danger">*</span>
</label>
<input type="number" class="form-control direccion-item" id="cp" th:field="*{cp}" min="1" max="99999"
required>
<div class="invalid-feedback"></div>
required th:classappend="${#fields.hasErrors('cp')} ? ' is-invalid'">
<div class="invalid-feedback" th:if="${#fields.hasErrors('cp')}" th:errors="*{cp}"></div>
</div>
<div class="form-group col-lg-6 col-md-6 col-sm-12 mr-0">
@ -46,21 +68,21 @@
<span th:text="#{direcciones.ciudad}">Ciudad</span>
<span class="text-danger">*</span>
</label>
<input class="form-control direccion-item" id="ciudad" th:field="*{ciudad}" maxlength="100" required>
<div class="invalid-feedback"></div>
<input class="form-control direccion-item" id="ciudad" th:field="*{ciudad}" maxlength="100" required
th:classappend="${#fields.hasErrors('ciudad')} ? ' is-invalid'">
<div class="invalid-feedback" th:if="${#fields.hasErrors('ciudad')}" th:errors="*{ciudad}"></div>
</div>
</div>
<div class="row">
<div class="row mt-2">
<div class="form-group col-lg-6 col-md-6 col-sm-12 ml-0">
<label for="provincia">
<span th:text="#{direcciones.provincia}">Provincia</span>
<span class="text-danger">*</span>
</label>
<input class="form-control direccion-item" id="provincia" th:field="*{provincia}" maxlength="100"
required>
<div class="invalid-feedback"></div>
required th:classappend="${#fields.hasErrors('provincia')} ? ' is-invalid'">
<div class="invalid-feedback" th:if="${#fields.hasErrors('provincia')}" th:errors="*{provincia}"></div>
</div>
<div class="form-group col-lg-6 col-md-6 col-sm-12 mr-0">
@ -68,67 +90,74 @@
<span th:text="#{direcciones.pais}">País</span>
<span class="text-danger">*</span>
</label>
<select class="form-control select2 direccion-item" id="pais" th:field="*{paisCode3}">
<select class="form-control select2 direccion-item" id="paisCode3" th:field="*{paisCode3}"
th:classappend="${#fields.hasErrors('paisCode3')} ? ' is-invalid'">
<option th:each="pais : ${paises}" th:value="${pais.id}" th:text="${pais.text}"
th:selected="${pais.id} == ${direccion.paisCode3}">
th:selected="${pais.id} == ${dirForm.paisCode3}">
</option>
</select>
<div class="invalid-feedback"></div>
<div class="invalid-feedback" th:if="${#fields.hasErrors('paisCode3')}" th:errors="*{paisCode3}"></div>
</div>
</div>
<div class="form-group">
<div class="form-group mt-2">
<label for="telefono">
<span th:text="#{direcciones.telefono}">Teléfono</span>
</label>
<input class="form-control direccion-item" id="telefono" th:field="*{telefono}" maxlength="50">
<div class="invalid-feedback"></div>
<input class="form-control direccion-item" id="telefono" th:field="*{telefono}" maxlength="50"
th:classappend="${#fields.hasErrors('telefono')} ? ' is-invalid'">
<div class="invalid-feedback" th:if="${#fields.hasErrors('telefono')}" th:errors="*{telefono}"></div>
</div>
<div class="form-group">
<div class="form-group mt-2">
<label for="instrucciones">
<span th:text="#{direcciones.instrucciones}">Instrucciones</span>
</label>
<textarea class="form-control direccion-item" id="instrucciones" th:field="*{instrucciones}" maxlength="255"
required style="max-height: 125px;"></textarea>
<div class="invalid-feedback"></div>
style="max-height: 125px;"
th:classappend="${#fields.hasErrors('instrucciones')} ? ' is-invalid'"></textarea>
<div class="invalid-feedback" th:if="${#fields.hasErrors('instrucciones')}" th:errors="*{instrucciones}">
</div>
</div>
<div class="form-check form-switch form-switch-custom my-2">
<input type="checkbox"
class="form-check-input form-switch-custom-primary direccion-item direccionFacturacion"
id="direccionFacturacion" name="direccionFacturacion" th:field="*{direccionFacturacion}">
<label for="direccionFacturacion" class="form-check-label" th:text="#{direcciones.isFacturacion}">Usar
también como
dirección de facturación</label>
id="direccionFacturacion" th:field="*{direccionFacturacion}">
<label for="direccionFacturacion" class="form-check-label" th:text="#{direcciones.isFacturacion}">
Usar también como dirección de facturación
</label>
</div>
<div
th:class="'form-group direccionFacturacionItems' + (${direccion != null and direccion.direccionFacturacion} ? '' : ' d-none')">
<label for="razon_social">
<label for="razonSocial">
<span th:text="#{direcciones.razon_social}">Razón Social</span>
<span class="text-danger">*</span>
</label>
<input class="form-control direccion-item" id="razonSocial" th:field="*{razonSocial}" maxlength="150">
<div class="invalid-feedback"></div>
<input class="form-control direccion-item" id="razonSocial" th:field="*{razonSocial}" maxlength="150"
th:classappend="${#fields.hasErrors('razonSocial')} ? ' is-invalid'">
<div class="invalid-feedback" th:if="${#fields.hasErrors('razonSocial')}" th:errors="*{razonSocial}"></div>
</div>
<div
th:class="'row direccionFacturacionItems' + (${direccion != null and direccion.direccionFacturacion} ? '' : ' d-none')">
th:class="'row mt-2 direccionFacturacionItems' + (${direccion != null and direccion.direccionFacturacion} ? '' : ' d-none')">
<div class="form-group col-lg-6 col-md-6 col-sm-12 ml-0">
<label for="tipoIdentificacionFiscal">
<span th:text="#{direcciones.tipo_identificacion_fiscal}">Tipo de identificación fiscal</span>
<span class="text-danger">*</span>
</label>
<select class="form-control select2 direccion-item" id="tipoIdentificacionFiscal"
th:field="*{tipoIdentificacionFiscal}">
th:field="*{tipoIdentificacionFiscal}"
th:classappend="${#fields.hasErrors('tipoIdentificacionFiscal')} ? ' is-invalid'">
<option th:value="DNI" th:text="#{direcciones.dni}">DNI</option>
<option th:value="NIE" th:text="#{direcciones.nie}">NIE</option>
<option th:value="Pasaporte" th:text="#{direcciones.pasaporte}">Pasaporte</option>
<option th:value="CIF" th:text="#{direcciones.cif}">CIF</option>
<option th:value="VAT_ID" th:text="#{direcciones.vat_id}">VAT ID</option>
</select>
<div class="invalid-feedback"></div>
<div class="invalid-feedback" th:if="${#fields.hasErrors('tipoIdentificacionFiscal')}"
th:errors="*{tipoIdentificacionFiscal}"></div>
</div>
<div class="form-group col-lg-6 col-md-6 col-sm-12 ml-0">
@ -137,13 +166,14 @@
<span class="text-danger">*</span>
</label>
<input class="form-control direccion-item" id="identificacionFiscal" th:field="*{identificacionFiscal}"
maxlength="50">
<div class="invalid-feedback"></div>
maxlength="50" th:classappend="${#fields.hasErrors('identificacionFiscal')} ? ' is-invalid'">
<div class="invalid-feedback" th:if="${#fields.hasErrors('identificacionFiscal')}"
th:errors="*{identificacionFiscal}"></div>
</div>
</div>
<div class="d-flex align-items-center justify-content-center">
<button type="submit" class="btn btn-secondary mt-3" th:text="#{direcciones.add}"></button>
<button type="submit" class="btn btn-secondary mt-3" th:text="#{direcciones.save}"></button>
</div>
</form>

View File

@ -49,7 +49,7 @@
<th scope="col" th:text="#{direcciones.tabla.id}">ID</th>
<th scope="col" th:text="#{direcciones.tabla.cliente}">Cliente</th>
<th scope="col" th:text="#{direcciones.alias}">Alias</th>
<th scope="col" th:text="#{direcciones.nombre}">Nombre y Apellidos</th>
<th scope="col" th:text="#{direcciones.tabla.att}">Att.</th>
<th scope="col" th:text="#{direcciones.direccion}">Dirección</th>
<th scope="col" th:text="#{direcciones.cp}">Código Postal</th>
<th scope="col" th:text="#{direcciones.ciudad}">Ciudad</th>