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; package com.imprimelibros.erp.config;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
@ -10,13 +11,16 @@ import jakarta.validation.ValidatorFactory;
@Configuration @Configuration
public class BeanValidationConfig { public class BeanValidationConfig {
// Asegura que usamos la factory de Spring (con SpringConstraintValidatorFactory) // Usa el MessageSource (messages*.properties) para resolver {códigos}
@Bean @Bean
public LocalValidatorFactoryBean validator() { public LocalValidatorFactoryBean validator(MessageSource messageSource) {
return new LocalValidatorFactoryBean(); 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 @Bean
public HibernatePropertiesCustomizer hibernateValidationCustomizer(ValidatorFactory vf) { public HibernatePropertiesCustomizer hibernateValidationCustomizer(ValidatorFactory vf) {
return props -> props.put("jakarta.persistence.validation.factory", 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.paises.Paises;
import com.imprimelibros.erp.users.User; import com.imprimelibros.erp.users.User;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.NotBlank;
@Entity @Entity
@Table(name = "direcciones", indexes = { @Table(name = "direcciones", indexes = {
@Index(name = "idx_direcciones_user", columnList = "user_id"), @Index(name = "idx_direcciones_user", columnList = "user_id"),
@ -42,32 +45,41 @@ public class Direccion extends AbstractAuditedEntity implements Serializable {
private Long id; private Long id;
// --- FK a users(id) // --- FK a users(id)
@NotNull(message = "{direcciones.form.error.required}")
@ManyToOne(fetch = FetchType.LAZY, optional = false) @ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "user_id", nullable = false) @JoinColumn(name = "user_id", nullable = false)
private User user; private User user;
@NotBlank(message = "{direcciones.form.error.required}")
@Column(name = "alias", length = 100, nullable = false) @Column(name = "alias", length = 100, nullable = false)
private String alias; private String alias;
@NotBlank(message = "{direcciones.form.error.required}")
@Column(name = "att", length = 150, nullable = false) @Column(name = "att", length = 150, nullable = false)
private String att; private String att;
@NotBlank(message = "{direcciones.form.error.required}")
@Column(name = "direccion", length = 255, nullable = false) @Column(name = "direccion", length = 255, nullable = false)
private String direccion; private String direccion;
@NotNull(message = "{direcciones.form.error.required}")
@Column(name = "cp", length = 20, nullable = false) @Column(name = "cp", length = 20, nullable = false)
private Integer cp; private Integer cp;
@NotBlank(message = "{direcciones.form.error.required}")
@Column(name = "ciudad", length = 100, nullable = false) @Column(name = "ciudad", length = 100, nullable = false)
private String ciudad; private String ciudad;
@NotBlank(message = "{direcciones.form.error.required}")
@Column(name = "provincia", length = 100, nullable = false) @Column(name = "provincia", length = 100, nullable = false)
private String provincia; private String provincia;
// Usamos el code3 del país como FK lógica (String) // 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) @Column(name = "pais_code3", length = 3, nullable = false)
private String paisCode3 = "esp"; private String paisCode3 = "esp";
@NotBlank(message = "{direcciones.form.error.required}")
@Column(name = "telefono", length = 30, nullable = false) @Column(name = "telefono", length = 30, nullable = false)
private String telefono; private String telefono;

View File

@ -12,6 +12,8 @@ import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.validation.BindingResult; import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping; 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.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; 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.DataTablesParser;
import com.imprimelibros.erp.datatables.DataTablesRequest; import com.imprimelibros.erp.datatables.DataTablesRequest;
import com.imprimelibros.erp.datatables.DataTablesResponse; import com.imprimelibros.erp.datatables.DataTablesResponse;
import com.imprimelibros.erp.i18n.TranslationService;
import com.imprimelibros.erp.paises.PaisesService; 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.persistence.criteria.Predicate;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
@Controller @Controller
@RequestMapping("/direcciones") @RequestMapping("/direcciones")
@ -34,11 +40,16 @@ public class DireccionController {
protected final DireccionRepository repo; protected final DireccionRepository repo;
protected final PaisesService paisesService; protected final PaisesService paisesService;
protected final MessageSource messageSource; 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.repo = repo;
this.paisesService = paisesService; this.paisesService = paisesService;
this.messageSource = messageSource; this.messageSource = messageSource;
this.userRepo = userRepo;
this.translationService = translationService;
} }
@GetMapping() @GetMapping()
@ -48,6 +59,19 @@ public class DireccionController {
.anyMatch(a -> a.getAuthority().equals("ROLE_USER")); .anyMatch(a -> a.getAuthority().equals("ROLE_USER"));
model.addAttribute("isUser", isUser ? 1 : 0); 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"; return "imprimelibros/direcciones/direccion-list";
} }
@ -80,13 +104,13 @@ public class DireccionController {
if (authentication != null && authentication.getAuthorities().stream() if (authentication != null && authentication.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_USER"))) { .anyMatch(a -> a.getAuthority().equals("ROLE_USER"))) {
String username = authentication.getName(); 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])); 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 // Construcción del datatable con entity + spec
return DataTable return DataTable
@ -168,11 +192,11 @@ public class DireccionController {
return "imprimelibros/direcciones/direccion-form :: direccionForm"; return "imprimelibros/direcciones/direccion-form :: direccionForm";
} }
model.addAttribute("direccion", opt.get()); model.addAttribute("dirForm", opt.get());
model.addAttribute("action", "/direcciones/" + id); model.addAttribute("action", "/direcciones/" + id);
} else { } else {
model.addAttribute("direccion", new Direccion()); model.addAttribute("dirForm", new Direccion());
model.addAttribute("action", "/direcciones"); model.addAttribute("action", "/direcciones");
} }
return "imprimelibros/direcciones/direccion-form :: direccionForm"; return "imprimelibros/direcciones/direccion-form :: direccionForm";
@ -180,7 +204,7 @@ public class DireccionController {
@PostMapping @PostMapping
public String create( public String create(
Direccion direccion, @Valid @ModelAttribute("dirForm") Direccion direccion,
BindingResult binding, BindingResult binding,
Model model, Model model,
HttpServletResponse response, HttpServletResponse response,
@ -188,130 +212,110 @@ public class DireccionController {
if (binding.hasErrors()) { if (binding.hasErrors()) {
response.setStatus(422); 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"; return "imprimelibros/direcciones/direccion-form :: direccionForm";
} }
var data = direccion; var data = direccion;
repo.save(data);
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";
}
response.setStatus(201); response.setStatus(201);
return null; return null;
} }
/*
@PutMapping("/{id}") @PostMapping("/{id}")
public String edit( public String update(
@PathVariable Long id, @PathVariable Long id,
MargenPresupuesto form, @Valid @ModelAttribute("dirForm") Direccion direccion, // <- nombre distinto
BindingResult binding, BindingResult binding,
Model model, Model model,
Authentication auth,
HttpServletResponse response, HttpServletResponse response,
Locale locale) { Locale locale) {
var uOpt = repo.findById(id); var opt = repo.findById(id);
if (uOpt.isEmpty()) { if (opt.isEmpty()) {
binding.reject("usuarios.error.noEncontrado", binding.reject("direcciones.error.noEncontrado",
messageSource.getMessage("usuarios.error.noEncontrado", null, locale)); 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()) { if (binding.hasErrors()) {
response.setStatus(422); response.setStatus(422);
model.addAttribute("action", "/configuracion/margenes-presupuesto/" + id); model.addAttribute("dirForm", direccion); // <- importante
return "imprimelibros/configuracion/margenes-presupuesto/margenes-presupuesto-form :: margenesPresupuestoForm"; model.addAttribute("paises", paisesService.getForSelect("", "", locale).get("results"));
model.addAttribute("action", "/direcciones/" + id);
return "imprimelibros/direcciones/direccion-form :: direccionForm";
} }
var entity = uOpt.get(); repo.save(direccion);
response.setStatus(200);
// 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);
return null; 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 -> { private boolean isOwnerOrAdmin(Authentication auth, Long ownerId) {
try { if (auth == null) {
u.setDeleted(true); return false;
u.setDeletedAt(LocalDateTime.now()); }
boolean isAdmin = auth.getAuthorities().stream()
repo.save(u); // ← NO delete(); guardamos el soft delete con deleted_by relleno .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
return ResponseEntity.ok(Map.of("message", if (isAdmin) {
messageSource.getMessage("margenes-presupuesto.exito.eliminado", null, locale))); return true;
} catch (Exception ex) { }
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) // Aquí deberías obtener el ID del usuario actual desde tu servicio de usuarios
.body(Map.of("message", Long currentUserId = null;
messageSource.getMessage("margenes-presupuesto.error.delete-internal-error", null, if (auth != null && auth.getPrincipal() instanceof UserDetailsImpl udi) {
locale))); currentUserId = udi.getId();
} } else if (auth != null) {
}).orElseGet(() -> ResponseEntity.status(HttpStatus.NOT_FOUND) currentUserId = userRepo.findIdByUserNameIgnoreCase(auth.getName()).orElse(null);
.body(Map.of("message", }
messageSource.getMessage("margenes-presupuesto.error.not-found", null, locale)))); return currentUserId != null && currentUserId.equals(ownerId);
} }
*/
} }

View File

@ -13,54 +13,22 @@ public interface DireccionRepository
extends JpaRepository<Direccion, Long>, extends JpaRepository<Direccion, Long>,
JpaSpecificationExecutor<Direccion> { JpaSpecificationExecutor<Direccion> {
@Query(""" @Query("""
SELECT select d from Direccion d
d.id AS id, left join fetch d.user
d.alias AS alias, left join fetch d.pais
d.att AS att, where d.user.id = :id
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)
""")
List<DireccionView> findAllWithPaisAndUser(@Param("userId") Long userId); List<DireccionView> findAllWithPaisAndUser(@Param("userId") Long userId);
//findbyidwithPaisAndUser
@Query(""" @Query("""
SELECT select d from Direccion d
d.id AS id, left join fetch d.user
d.alias AS alias, left join fetch d.pais
d.att AS att, where d.id = :id
d.direccion AS direccion, """)
d.cp AS cp, Optional<Direccion> findByIdWithPaisAndUser(@Param("id") Long id);
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);

View File

@ -2,13 +2,14 @@ package com.imprimelibros.erp.direcciones;
public interface DireccionView { public interface DireccionView {
Long getId(); Long getId();
UserView getUser();
String getAlias(); String getAlias();
String getAtt(); String getAtt();
String getDireccion(); String getDireccion();
String getCp(); String getCp();
String getCiudad(); String getCiudad();
String getProvincia(); String getProvincia();
String getPaisCode3(); PaisView getPais();
String getPaisKeyword(); String getPaisKeyword();
String getTelefono(); String getTelefono();
Boolean getIsFacturacion(); Boolean getIsFacturacion();
@ -16,4 +17,12 @@ public interface DireccionView {
String getTipoIdentificacionFiscal(); String getTipoIdentificacionFiscal();
String getIdentificacionFiscal(); String getIdentificacionFiscal();
String getCliente(); 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 # Hibernate Timezone
# #
spring.jpa.properties.hibernate.jdbc.time_zone=UTC spring.jpa.properties.hibernate.jdbc.time_zone=UTC
# #
# PDF Templates # PDF Templates
# #

View File

@ -4,6 +4,8 @@ direcciones.editar=Editar dirección
direcciones.breadcrumb=Direcciones direcciones.breadcrumb=Direcciones
direcciones.add=Añadir dirección direcciones.add=Añadir dirección
direcciones.edit=Editar dirección direcciones.edit=Editar dirección
direcciones.save=Guardar dirección
direcciones.user=Cliente
direcciones.alias=Alias direcciones.alias=Alias
direcciones.alias-descripcion=Nombre descriptivo para identificar la dirección. direcciones.alias-descripcion=Nombre descriptivo para identificar la dirección.
direcciones.nombre=Nombre y Apellidos 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.identificacion_fiscal=Número de identificación fiscal
direcciones.tabla.id=ID direcciones.tabla.id=ID
direcciones.tabla.att=Att.
direcciones.tabla.cliente=Cliente direcciones.tabla.cliente=Cliente
direcciones.tabla.acciones=Acciones direcciones.tabla.acciones=Acciones
@ -30,5 +33,15 @@ direcciones.pasaporte=Pasaporte
direcciones.cif=C.I.F. direcciones.cif=C.I.F.
direcciones.vat_id=VAT ID 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, pageLength: 50,
language: { url: '/assets/libs/datatables/i18n/' + language + '.json' }, language: { url: '/assets/libs/datatables/i18n/' + language + '.json' },
responsive: true, responsive: true,
dom: 'lrBtip', dom: $('#isUser').val() == 1 ? 'lrtip' : 'lrBtip',
buttons: { buttons: {
dom: { dom: {
button: { button: {
@ -49,10 +49,10 @@
}, },
order: [[0, 'asc']], order: [[0, 'asc']],
columns: [ columns: [
{ data: 'id', name: 'id', orderable: true, visible: $('#isUser').val() }, { data: 'id', name: 'id', orderable: true, visible: $('#isUser').val() == 1 ? false : true },
{ data: 'cliente', name: 'cliente', orderable: true }, { data: 'cliente', name: 'cliente', orderable: true, visible: $('#isUser').val() == 1 ? false : true },
{ data: 'alias', name: 'alias', orderable: 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: 'direccion', name: 'direccion', orderable: true },
{ data: 'cp', name: 'cp', orderable: true }, { data: 'cp', name: 'cp', orderable: true },
{ data: 'ciudad', name: 'ciudad', orderable: true }, { data: 'ciudad', name: 'ciudad', orderable: true },
@ -77,7 +77,7 @@
$(document).on("change", ".direccionFacturacion", function () { $(document).on("change", ".direccionFacturacion", function () {
const isChecked = $(this).is(':checked'); const isChecked = $(this).is(':checked');
if(isChecked) { if (isChecked) {
$('.direccionFacturacionItems').removeClass('d-none'); $('.direccionFacturacionItems').removeClass('d-none');
} else { } else {
$('.direccionFacturacionItems').addClass('d-none'); $('.direccionFacturacionItems').addClass('d-none');
@ -95,24 +95,50 @@
const title = $('#direccionFormModalBody #direccionForm').data('add'); const title = $('#direccionFormModalBody #direccionForm').data('add');
$('#direccionFormModal .modal-title').text(title); $('#direccionFormModal .modal-title').text(title);
modal.show(); 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" // Abrir "Editar"
$(document).on('click', '.btn-edit-direccion', function (e) { $(document).on('click', '.btn-edit-direccion', function (e) {
e.preventDefault(); e.preventDefault();
const id = $(this).data('id'); const id = $(this).data('id');
/*$.get('/configuracion/margenes-presupuesto/form', { id }, function (html) { e.preventDefault();
$('#margenesPresupuestoModalBody').html(html); $.get('/direcciones/form', { id }, function (html) {
const title = $('#margenesPresupuestoModalBody #margenesPresupuestoForm').data('edit'); $('#direccionFormModalBody').html(html);
$('#margenesPresupuestoModal .modal-title').text(title); const title = $('#direccionFormModalBody #direccionForm').data('edit');
modal.show();*/ $('#direccionFormModal .modal-title').text(title);
modal.show();
initSelect2Cliente(true);
});
}); });
// Botón "Eliminar" // Botón "Eliminar"
$(document).on('click', '.btn-delete-margen', function (e) { $(document).on('click', '.btn-delete-direccion', function (e) {
e.preventDefault(); e.preventDefault();
const id = $(this).data('id'); const id = $(this).data('id');
@ -132,12 +158,12 @@
if (!result.isConfirmed) return; if (!result.isConfirmed) return;
$.ajax({ $.ajax({
url: '/configuracion/margenes-presupuesto/' + id, url: '/direcciones/' + id,
type: 'DELETE', type: 'DELETE',
success: function () { success: function () {
Swal.fire({ Swal.fire({
icon: 'success', title: window.languageBundle.get(['margenes-presupuesto.delete.ok.title']) || 'Eliminado', icon: 'success', title: window.languageBundle.get(['direcciones.delete.ok.title']) || 'Eliminado',
text: window.languageBundle.get(['margenes-presupuesto.delete.ok.text']) || 'El margen ha sido eliminado con éxito.', text: window.languageBundle.get(['direcciones.delete.ok.text']) || 'La dirección ha sido eliminada con éxito.',
showConfirmButton: true, showConfirmButton: true,
customClass: { customClass: {
confirmButton: 'btn btn-secondary w-xs mt-2', confirmButton: 'btn btn-secondary w-xs mt-2',
@ -148,7 +174,7 @@
error: function (xhr) { error: function (xhr) {
// usa el mensaje del backend; fallback genérico por si no llega JSON // usa el mensaje del backend; fallback genérico por si no llega JSON
const msg = (xhr.responseJSON && xhr.responseJSON.message) 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 }); Swal.fire({ icon: 'error', title: 'No se pudo eliminar', text: msg });
} }
}); });
@ -156,7 +182,7 @@
}); });
// Submit del form en el modal // Submit del form en el modal
$(document).on('submit', '#margenesPresupuestoForm', function (e) { $(document).on('submit', '#direccionForm', function (e) {
e.preventDefault(); e.preventDefault();
const $form = $(this); const $form = $(this);
@ -167,11 +193,11 @@
dataType: 'html', dataType: 'html',
success: function (html) { success: function (html) {
// Si por cualquier motivo llega 200 con fragmento, lo insertamos igual // Si por cualquier motivo llega 200 con fragmento, lo insertamos igual
if (typeof html === 'string' && html.indexOf('id="margenesPresupuestoForm"') !== -1 && html.indexOf('<html') === -1) { if (typeof html === 'string' && html.indexOf('id="direccionForm"') !== -1 && html.indexOf('<html') === -1) {
$('#margenesPresupuestoModalBody').html(html); $('#direccionFormModalBody').html(html);
const isEdit = $('#margenesPresupuestoModalBody #margenesPresupuestoForm input[name="_method"][value="PUT"]').length > 0; const isEdit = $('#direccionFormModalBody #direccionForm input[name="_method"][value="PUT"]').length > 0;
const title = $('#margenesPresupuestoModalBody #margenesPresupuestoForm').data(isEdit ? 'edit' : 'add'); const title = $('#direccionFormModalBody #direccionForm').data(isEdit ? 'edit' : 'add');
$('#margenesPresupuestoModal .modal-title').text(title); $('#direccionModal .modal-title').text(title);
return; return;
} }
// Éxito real: cerrar y recargar tabla // Éxito real: cerrar y recargar tabla
@ -181,14 +207,15 @@
error: function (xhr) { error: function (xhr) {
// Con 422 devolvemos el fragmento con errores aquí // Con 422 devolvemos el fragmento con errores aquí
if (xhr.status === 422 && xhr.responseText) { if (xhr.status === 422 && xhr.responseText) {
$('#margenesPresupuestoModalBody').html(xhr.responseText); $('#direccionFormModalBody').html(xhr.responseText);
const isEdit = $('#margenesPresupuestoModalBody #margenesPresupuestoForm input[name="_method"][value="PUT"]').length > 0; const isEdit = $('#direccionFormModalBody #direccionForm input[name="_method"][value="PUT"]').length > 0;
const title = $('#margenesPresupuestoModalBody #margenesPresupuestoForm').data(isEdit ? 'edit' : 'add'); const title = $('#direccionFormModalBody #direccionForm').data(isEdit ? 'edit' : 'add');
$('#margenesPresupuestoModal .modal-title').text(title); $('#direccionModal .modal-title').text(title);
initSelect2Cliente(true);
return; return;
} }
// Fallback // 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"> <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}"> 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"> <label for="alias">
<span th:text="#{direcciones.alias}">Alias</span> <span th:text="#{direcciones.alias}">Alias</span>
<span class="text-danger">*</span> <span class="text-danger">*</span>
</label> </label>
<input class="form-control direccion-item" id="alias" th:field="*{alias}" maxlength="100" required> <input class="form-control direccion-item" id="alias" th:field="*{alias}" maxlength="100" required
<div class="invalid-feedback"></div> 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> <label th:text="#{direcciones.alias-descripcion}" class="form-text text-muted"></label>
</div> </div>
<div class="form-group"> <div class="form-group mt-2">
<label for="att"> <label for="att">
<span th:text="#{direcciones.nombre}">Nombre y Apellidos</span> <span th:text="#{direcciones.nombre}">Nombre y Apellidos</span>
<span class="text-danger">*</span> <span class="text-danger">*</span>
</label> </label>
<input class="form-control direccion-item" id="att" th:field="*{att}" maxlength="150" required> <input class="form-control direccion-item" id="att" th:field="*{att}" maxlength="150" required
<div class="invalid-feedback"></div> th:classappend="${#fields.hasErrors('att')} ? ' is-invalid'">
<div class="invalid-feedback" th:if="${#fields.hasErrors('att')}" th:errors="*{att}"></div>
</div> </div>
<div class="form-group"> <div class="form-group mt-2">
<label for="direccion"> <label for="direccion">
<span th:text="#{direcciones.direccion}">Dirección</span> <span th:text="#{direcciones.direccion}">Dirección</span>
<span class="text-danger">*</span> <span class="text-danger">*</span>
</label> </label>
<textarea class="form-control direccion-item" id="direccion" th:field="*{direccion}" maxlength="255" <textarea class="form-control direccion-item" id="direccion" th:field="*{direccion}" maxlength="255"
required style="max-height: 125px;"></textarea> required style="max-height: 125px;"
<div class="invalid-feedback"></div> th:classappend="${#fields.hasErrors('direccion')} ? ' is-invalid'"></textarea>
<div class="invalid-feedback" th:if="${#fields.hasErrors('direccion')}" th:errors="*{direccion}"></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"> <div class="form-group col-lg-6 col-md-6 col-sm-12 ml-0">
<label for="cp"> <label for="cp">
<span th:text="#{direcciones.cp}">Código Postal</span> <span th:text="#{direcciones.cp}">Código Postal</span>
<span class="text-danger">*</span> <span class="text-danger">*</span>
</label> </label>
<input type="number" class="form-control direccion-item" id="cp" th:field="*{cp}" min="1" max="99999" <input type="number" class="form-control direccion-item" id="cp" th:field="*{cp}" min="1" max="99999"
required> required th:classappend="${#fields.hasErrors('cp')} ? ' is-invalid'">
<div class="invalid-feedback"></div> <div class="invalid-feedback" th:if="${#fields.hasErrors('cp')}" th:errors="*{cp}"></div>
</div> </div>
<div class="form-group col-lg-6 col-md-6 col-sm-12 mr-0"> <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 th:text="#{direcciones.ciudad}">Ciudad</span>
<span class="text-danger">*</span> <span class="text-danger">*</span>
</label> </label>
<input class="form-control direccion-item" id="ciudad" th:field="*{ciudad}" maxlength="100" required> <input class="form-control direccion-item" id="ciudad" th:field="*{ciudad}" maxlength="100" required
<div class="invalid-feedback"></div> th:classappend="${#fields.hasErrors('ciudad')} ? ' is-invalid'">
<div class="invalid-feedback" th:if="${#fields.hasErrors('ciudad')}" th:errors="*{ciudad}"></div>
</div> </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"> <div class="form-group col-lg-6 col-md-6 col-sm-12 ml-0">
<label for="provincia"> <label for="provincia">
<span th:text="#{direcciones.provincia}">Provincia</span> <span th:text="#{direcciones.provincia}">Provincia</span>
<span class="text-danger">*</span> <span class="text-danger">*</span>
</label> </label>
<input class="form-control direccion-item" id="provincia" th:field="*{provincia}" maxlength="100" <input class="form-control direccion-item" id="provincia" th:field="*{provincia}" maxlength="100"
required> required th:classappend="${#fields.hasErrors('provincia')} ? ' is-invalid'">
<div class="invalid-feedback"></div> <div class="invalid-feedback" th:if="${#fields.hasErrors('provincia')}" th:errors="*{provincia}"></div>
</div> </div>
<div class="form-group col-lg-6 col-md-6 col-sm-12 mr-0"> <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 th:text="#{direcciones.pais}">País</span>
<span class="text-danger">*</span> <span class="text-danger">*</span>
</label> </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}" <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> </option>
</select> </select>
<div class="invalid-feedback"></div> <div class="invalid-feedback" th:if="${#fields.hasErrors('paisCode3')}" th:errors="*{paisCode3}"></div>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group mt-2">
<label for="telefono"> <label for="telefono">
<span th:text="#{direcciones.telefono}">Teléfono</span> <span th:text="#{direcciones.telefono}">Teléfono</span>
</label> </label>
<input class="form-control direccion-item" id="telefono" th:field="*{telefono}" maxlength="50"> <input class="form-control direccion-item" id="telefono" th:field="*{telefono}" maxlength="50"
<div class="invalid-feedback"></div> th:classappend="${#fields.hasErrors('telefono')} ? ' is-invalid'">
<div class="invalid-feedback" th:if="${#fields.hasErrors('telefono')}" th:errors="*{telefono}"></div>
</div> </div>
<div class="form-group"> <div class="form-group mt-2">
<label for="instrucciones"> <label for="instrucciones">
<span th:text="#{direcciones.instrucciones}">Instrucciones</span> <span th:text="#{direcciones.instrucciones}">Instrucciones</span>
</label> </label>
<textarea class="form-control direccion-item" id="instrucciones" th:field="*{instrucciones}" maxlength="255" <textarea class="form-control direccion-item" id="instrucciones" th:field="*{instrucciones}" maxlength="255"
required style="max-height: 125px;"></textarea> style="max-height: 125px;"
<div class="invalid-feedback"></div> th:classappend="${#fields.hasErrors('instrucciones')} ? ' is-invalid'"></textarea>
<div class="invalid-feedback" th:if="${#fields.hasErrors('instrucciones')}" th:errors="*{instrucciones}">
</div>
</div> </div>
<div class="form-check form-switch form-switch-custom my-2"> <div class="form-check form-switch form-switch-custom my-2">
<input type="checkbox" <input type="checkbox"
class="form-check-input form-switch-custom-primary direccion-item direccionFacturacion" class="form-check-input form-switch-custom-primary direccion-item direccionFacturacion"
id="direccionFacturacion" name="direccionFacturacion" th:field="*{direccionFacturacion}"> id="direccionFacturacion" th:field="*{direccionFacturacion}">
<label for="direccionFacturacion" class="form-check-label" th:text="#{direcciones.isFacturacion}">Usar <label for="direccionFacturacion" class="form-check-label" th:text="#{direcciones.isFacturacion}">
también como Usar también como dirección de facturación
dirección de facturación</label> </label>
</div> </div>
<div <div
th:class="'form-group direccionFacturacionItems' + (${direccion != null and direccion.direccionFacturacion} ? '' : ' d-none')"> 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 th:text="#{direcciones.razon_social}">Razón Social</span>
<span class="text-danger">*</span> <span class="text-danger">*</span>
</label> </label>
<input class="form-control direccion-item" id="razonSocial" th:field="*{razonSocial}" maxlength="150"> <input class="form-control direccion-item" id="razonSocial" th:field="*{razonSocial}" maxlength="150"
<div class="invalid-feedback"></div> th:classappend="${#fields.hasErrors('razonSocial')} ? ' is-invalid'">
<div class="invalid-feedback" th:if="${#fields.hasErrors('razonSocial')}" th:errors="*{razonSocial}"></div>
</div> </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"> <div class="form-group col-lg-6 col-md-6 col-sm-12 ml-0">
<label for="tipoIdentificacionFiscal"> <label for="tipoIdentificacionFiscal">
<span th:text="#{direcciones.tipo_identificacion_fiscal}">Tipo de identificación fiscal</span> <span th:text="#{direcciones.tipo_identificacion_fiscal}">Tipo de identificación fiscal</span>
<span class="text-danger">*</span> <span class="text-danger">*</span>
</label> </label>
<select class="form-control select2 direccion-item" id="tipoIdentificacionFiscal" <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="DNI" th:text="#{direcciones.dni}">DNI</option>
<option th:value="NIE" th:text="#{direcciones.nie}">NIE</option> <option th:value="NIE" th:text="#{direcciones.nie}">NIE</option>
<option th:value="Pasaporte" th:text="#{direcciones.pasaporte}">Pasaporte</option> <option th:value="Pasaporte" th:text="#{direcciones.pasaporte}">Pasaporte</option>
<option th:value="CIF" th:text="#{direcciones.cif}">CIF</option> <option th:value="CIF" th:text="#{direcciones.cif}">CIF</option>
<option th:value="VAT_ID" th:text="#{direcciones.vat_id}">VAT ID</option> <option th:value="VAT_ID" th:text="#{direcciones.vat_id}">VAT ID</option>
</select> </select>
<div class="invalid-feedback"></div> <div class="invalid-feedback" th:if="${#fields.hasErrors('tipoIdentificacionFiscal')}"
th:errors="*{tipoIdentificacionFiscal}"></div>
</div> </div>
<div class="form-group col-lg-6 col-md-6 col-sm-12 ml-0"> <div class="form-group col-lg-6 col-md-6 col-sm-12 ml-0">
@ -137,13 +166,14 @@
<span class="text-danger">*</span> <span class="text-danger">*</span>
</label> </label>
<input class="form-control direccion-item" id="identificacionFiscal" th:field="*{identificacionFiscal}" <input class="form-control direccion-item" id="identificacionFiscal" th:field="*{identificacionFiscal}"
maxlength="50"> maxlength="50" th:classappend="${#fields.hasErrors('identificacionFiscal')} ? ' is-invalid'">
<div class="invalid-feedback"></div> <div class="invalid-feedback" th:if="${#fields.hasErrors('identificacionFiscal')}"
th:errors="*{identificacionFiscal}"></div>
</div> </div>
</div> </div>
<div class="d-flex align-items-center justify-content-center"> <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> </div>
</form> </form>

View File

@ -49,7 +49,7 @@
<th scope="col" th:text="#{direcciones.tabla.id}">ID</th> <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.tabla.cliente}">Cliente</th>
<th scope="col" th:text="#{direcciones.alias}">Alias</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.direccion}">Dirección</th>
<th scope="col" th:text="#{direcciones.cp}">Código Postal</th> <th scope="col" th:text="#{direcciones.cp}">Código Postal</th>
<th scope="col" th:text="#{direcciones.ciudad}">Ciudad</th> <th scope="col" th:text="#{direcciones.ciudad}">Ciudad</th>