mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-01-13 00:48:49 +00:00
Merge branch 'feat/direcciones_cliente' into 'main'
Feat/direcciones cliente See merge request jjimenez/erp-imprimelibros!18
This commit is contained in:
@ -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);
|
||||
|
||||
@ -146,11 +146,11 @@ public class MargenPresupuestoController {
|
||||
if (id != null) {
|
||||
var opt = repo.findById(id);
|
||||
if (opt.isEmpty()) {
|
||||
binding.reject("usuarios.error.noEncontrado",
|
||||
messageSource.getMessage("usuarios.error.noEncontrado", null, locale));
|
||||
binding.reject("margenes-presupuesto.error.noEncontrado",
|
||||
messageSource.getMessage("margenes-presupuesto.error.noEncontrado", null, locale));
|
||||
response.setStatus(404);
|
||||
model.addAttribute("action", "/users/" + id);
|
||||
return "imprimelibros/users/user-form :: userForm";
|
||||
model.addAttribute("action", "/configuracion/margenes-presupuesto/" + id);
|
||||
return "imprimelibros/configuracion/margenes-presupuesto/margenes-presupuesto-form :: margenesPresupuestoForm";
|
||||
}
|
||||
|
||||
model.addAttribute("margenPresupuesto", opt.get());
|
||||
|
||||
237
src/main/java/com/imprimelibros/erp/direcciones/Direccion.java
Normal file
237
src/main/java/com/imprimelibros/erp/direcciones/Direccion.java
Normal file
@ -0,0 +1,237 @@
|
||||
package com.imprimelibros.erp.direcciones;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.hibernate.annotations.SQLDelete;
|
||||
import org.hibernate.annotations.SQLRestriction;
|
||||
|
||||
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"),
|
||||
@Index(name = "idx_direcciones_pais_code3", columnList = "pais_code3")
|
||||
})
|
||||
@SQLDelete(sql = "UPDATE direcciones SET deleted = 1, deleted_at = NOW(3) WHERE id = ?")
|
||||
@SQLRestriction("deleted = 0")
|
||||
public class Direccion extends AbstractAuditedEntity implements Serializable {
|
||||
|
||||
public enum TipoIdentificacionFiscal {
|
||||
DNI("direcciones.dni"),
|
||||
NIE("direcciones.nie"),
|
||||
CIF("direcciones.cif"),
|
||||
Pasaporte("direcciones.pasaporte"),
|
||||
VAT_ID("direcciones.vat_id");
|
||||
|
||||
private String key;
|
||||
|
||||
TipoIdentificacionFiscal(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
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;
|
||||
|
||||
@Column(name = "instrucciones", length = 255)
|
||||
private String instrucciones;
|
||||
|
||||
@Column(name = "is_facturacion", nullable = false)
|
||||
private boolean direccionFacturacion = false;
|
||||
|
||||
@Column(name = "razon_social", length = 150)
|
||||
private String razonSocial;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "tipo_identificacion_fiscal", length = 20, nullable = false)
|
||||
private TipoIdentificacionFiscal tipoIdentificacionFiscal = TipoIdentificacionFiscal.DNI;
|
||||
|
||||
@Column(name = "identificacion_fiscal", length = 50)
|
||||
private String identificacionFiscal;
|
||||
|
||||
// --- Asociación opcional (read-only) a Pais por code3, si tienes la entidad
|
||||
// Pais
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "pais_code3", referencedColumnName = "code3", insertable = false, updatable = false)
|
||||
private Paises pais;
|
||||
|
||||
// --- Getters & Setters ---
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public void setUser(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
public String getAlias() {
|
||||
return alias;
|
||||
}
|
||||
|
||||
public void setAlias(String alias) {
|
||||
this.alias = alias;
|
||||
}
|
||||
|
||||
public String getAtt() {
|
||||
return att;
|
||||
}
|
||||
|
||||
public void setAtt(String att) {
|
||||
this.att = att;
|
||||
}
|
||||
|
||||
public String getDireccion() {
|
||||
return direccion;
|
||||
}
|
||||
|
||||
public void setDireccion(String direccion) {
|
||||
this.direccion = direccion;
|
||||
}
|
||||
|
||||
public Integer getCp() {
|
||||
return cp;
|
||||
}
|
||||
|
||||
public void setCp(Integer cp) {
|
||||
this.cp = cp;
|
||||
}
|
||||
|
||||
public String getCiudad() {
|
||||
return ciudad;
|
||||
}
|
||||
|
||||
public void setCiudad(String ciudad) {
|
||||
this.ciudad = ciudad;
|
||||
}
|
||||
|
||||
public String getProvincia() {
|
||||
return provincia;
|
||||
}
|
||||
|
||||
public void setProvincia(String provincia) {
|
||||
this.provincia = provincia;
|
||||
}
|
||||
|
||||
public String getPaisCode3() {
|
||||
return paisCode3;
|
||||
}
|
||||
|
||||
public void setPaisCode3(String paisCode3) {
|
||||
this.paisCode3 = paisCode3;
|
||||
}
|
||||
|
||||
public String getTelefono() {
|
||||
return telefono;
|
||||
}
|
||||
|
||||
public void setTelefono(String telefono) {
|
||||
this.telefono = telefono;
|
||||
}
|
||||
|
||||
public String getInstrucciones() {
|
||||
return instrucciones;
|
||||
}
|
||||
|
||||
public void setInstrucciones(String instrucciones) {
|
||||
this.instrucciones = instrucciones;
|
||||
}
|
||||
|
||||
public boolean isDireccionFacturacion() {
|
||||
return direccionFacturacion;
|
||||
}
|
||||
|
||||
public void setDireccionFacturacion(boolean direccionFacturacion) {
|
||||
this.direccionFacturacion = direccionFacturacion;
|
||||
}
|
||||
|
||||
public String getRazonSocial() {
|
||||
return razonSocial;
|
||||
}
|
||||
|
||||
public void setRazonSocial(String razonSocial) {
|
||||
this.razonSocial = razonSocial;
|
||||
}
|
||||
|
||||
public TipoIdentificacionFiscal getTipoIdentificacionFiscal() {
|
||||
return tipoIdentificacionFiscal;
|
||||
}
|
||||
|
||||
public void setTipoIdentificacionFiscal(TipoIdentificacionFiscal tipo) {
|
||||
this.tipoIdentificacionFiscal = tipo;
|
||||
}
|
||||
|
||||
public String getIdentificacionFiscal() {
|
||||
return identificacionFiscal;
|
||||
}
|
||||
|
||||
public void setIdentificacionFiscal(String identificacionFiscal) {
|
||||
this.identificacionFiscal = identificacionFiscal;
|
||||
}
|
||||
|
||||
public Paises getPais() {
|
||||
return pais;
|
||||
}
|
||||
|
||||
public void setPais(Paises pais) {
|
||||
this.pais = pais;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,437 @@
|
||||
package com.imprimelibros.erp.direcciones;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
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;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
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.User;
|
||||
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")
|
||||
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, UserDao userRepo, TranslationService translationService) {
|
||||
this.repo = repo;
|
||||
this.paisesService = paisesService;
|
||||
this.messageSource = messageSource;
|
||||
this.userRepo = userRepo;
|
||||
this.translationService = translationService;
|
||||
}
|
||||
|
||||
@GetMapping()
|
||||
public String viewDirecciones(Model model, Authentication auth, Locale locale) {
|
||||
|
||||
boolean isUser = auth != null && auth.getAuthorities().stream()
|
||||
.anyMatch(a -> a.getAuthority().equals("ROLE_USER"));
|
||||
model.addAttribute("isUser", isUser ? 1 : 0);
|
||||
|
||||
List<String> keys = List.of(
|
||||
"direcciones.delete.title",
|
||||
"direcciones.delete.text",
|
||||
"direcciones.eliminar",
|
||||
"direcciones.delete.button",
|
||||
"app.yes",
|
||||
"app.cancelar",
|
||||
"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);
|
||||
|
||||
if (isUser)
|
||||
return "imprimelibros/direcciones/direccion-list-cliente";
|
||||
else
|
||||
return "imprimelibros/direcciones/direccion-list";
|
||||
}
|
||||
|
||||
@GetMapping(value = "/datatable", produces = "application/json")
|
||||
@ResponseBody
|
||||
public DataTablesResponse<Map<String, Object>> datatable(
|
||||
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",
|
||||
"cliente", "alias",
|
||||
"att", "direccion", "cp", "ciudad", "provincia", "pais");
|
||||
|
||||
List<String> orderable = List.of(
|
||||
"id",
|
||||
"cliente", "alias",
|
||||
"att", "direccion", "cp", "ciudad", "provincia", "pais");
|
||||
|
||||
// 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())
|
||||
|
||||
// Columnas calculadas:
|
||||
|
||||
// cliente = nombre del usuario (o username si no tienes name)
|
||||
.add("cliente", d -> {
|
||||
var u = d.getUser();
|
||||
return (u != null && u.getFullName() != null && !u.getFullName().isBlank())
|
||||
? u.getFullName()
|
||||
: "";
|
||||
})
|
||||
|
||||
// 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);
|
||||
})
|
||||
|
||||
// Ejemplo de columna de acciones:
|
||||
.add("actions", d -> """
|
||||
<div class="hstack gap-3 flex-wrap">
|
||||
<a href="javascript:void(0);" data-id="%d" class="link-success btn-edit-direccion fs-15">
|
||||
<i class="ri-edit-2-line"></i>
|
||||
</a>
|
||||
<a href="javascript:void(0);" data-id="%d" class="link-danger btn-delete-direccion fs-15">
|
||||
<i class="ri-delete-bin-5-line"></i>
|
||||
</a>
|
||||
</div>
|
||||
""".formatted(d.getId(), d.getId()))
|
||||
|
||||
// WHERE dinámico (spec base)
|
||||
.where(base)
|
||||
|
||||
// Si tu DataTable helper soporta “join/alias” para buscar/ordenar por campos
|
||||
// relacionados:
|
||||
// .searchAlias("cliente", (root, cb) -> root.join("user").get("name"))
|
||||
// .orderAlias("cliente", (root) -> root.join("user").get("name"))
|
||||
// .searchAlias("pais", (root, cb) -> root.join("pais",
|
||||
// JoinType.LEFT).get("keyword"))
|
||||
// .orderAlias("pais", (root) -> root.join("pais",
|
||||
// JoinType.LEFT).get("keyword"))
|
||||
|
||||
.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"));
|
||||
|
||||
if (id != null) {
|
||||
var opt = repo.findByIdWithPaisAndUser(id);
|
||||
if (opt == null) {
|
||||
binding.reject("direcciones.error.noEncontrado",
|
||||
messageSource.getMessage("direcciones.error.noEncontrado", null, locale));
|
||||
response.setStatus(404);
|
||||
model.addAttribute("action", "/direcciones/" + id);
|
||||
return "imprimelibros/direcciones/direccion-form :: direccionForm";
|
||||
}
|
||||
|
||||
model.addAttribute("dirForm", opt.get());
|
||||
model.addAttribute("action", "/direcciones/" + id);
|
||||
} else {
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public String create(
|
||||
@Valid @ModelAttribute("dirForm") Direccion direccion,
|
||||
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"));
|
||||
model.addAttribute("action", "/direcciones");
|
||||
model.addAttribute("dirForm", direccion);
|
||||
return "imprimelibros/direcciones/direccion-form :: direccionForm";
|
||||
}
|
||||
|
||||
var data = direccion;
|
||||
|
||||
repo.save(data);
|
||||
response.setStatus(201);
|
||||
return null;
|
||||
}
|
||||
|
||||
@PostMapping("/{id}")
|
||||
public String update(
|
||||
@PathVariable Long id,
|
||||
@Valid @ModelAttribute("dirForm") Direccion direccion, // <- nombre distinto
|
||||
BindingResult binding,
|
||||
Model model,
|
||||
Authentication auth,
|
||||
HttpServletResponse response,
|
||||
Locale 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("dirForm", direccion); // <- importante
|
||||
model.addAttribute("paises", paisesService.getForSelect("", "", locale).get("results"));
|
||||
model.addAttribute("action", "/direcciones/" + id);
|
||||
return "imprimelibros/direcciones/direccion-form :: direccionForm";
|
||||
}
|
||||
|
||||
repo.save(direccion);
|
||||
response.setStatus(200);
|
||||
return null;
|
||||
}
|
||||
|
||||
@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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
package com.imprimelibros.erp.direcciones;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface DireccionRepository
|
||||
extends JpaRepository<Direccion, Long>,
|
||||
JpaSpecificationExecutor<Direccion> {
|
||||
|
||||
|
||||
@Query("""
|
||||
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);
|
||||
|
||||
@Query("""
|
||||
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);
|
||||
|
||||
|
||||
|
||||
@Query(value = "SELECT * FROM direcciones", nativeQuery = true)
|
||||
List<DireccionView> findAllWithDeleted();
|
||||
|
||||
// find by user_id
|
||||
List<Direccion> findByUserId(Long userId);
|
||||
|
||||
// find by user_id with deleted
|
||||
@Query(value = "SELECT * FROM direcciones WHERE user_id = :userId", nativeQuery = true)
|
||||
List<Direccion> findByUserIdWithDeleted(@Param("userId") Long userId);
|
||||
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package com.imprimelibros.erp.direcciones;
|
||||
|
||||
public interface DireccionView {
|
||||
Long getId();
|
||||
UserView getUser();
|
||||
String getAlias();
|
||||
String getAtt();
|
||||
String getDireccion();
|
||||
String getCp();
|
||||
String getCiudad();
|
||||
String getProvincia();
|
||||
PaisView getPais();
|
||||
String getPaisKeyword();
|
||||
String getTelefono();
|
||||
Boolean getIsFacturacion();
|
||||
String getRazonSocial();
|
||||
String getTipoIdentificacionFiscal();
|
||||
String getIdentificacionFiscal();
|
||||
String getCliente();
|
||||
interface UserView {
|
||||
Long getId();
|
||||
String getFullName();
|
||||
}
|
||||
interface PaisView {
|
||||
String getCode3();
|
||||
String getKeyword();
|
||||
}
|
||||
}
|
||||
82
src/main/java/com/imprimelibros/erp/paises/Paises.java
Normal file
82
src/main/java/com/imprimelibros/erp/paises/Paises.java
Normal file
@ -0,0 +1,82 @@
|
||||
package com.imprimelibros.erp.paises;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
@Entity
|
||||
@Table(name = "paises")
|
||||
public class Paises {
|
||||
|
||||
@Id
|
||||
@Column(name = "keyword", length = 64, nullable = false)
|
||||
private String keyword;
|
||||
|
||||
@Column(name = "code", length = 2, nullable = false, unique = true)
|
||||
private String code;
|
||||
|
||||
@Column(name = "code3", length = 3, nullable = false, unique = true)
|
||||
private String code3;
|
||||
|
||||
@Column(name = "currency", length = 3, nullable = false)
|
||||
private String currency;
|
||||
|
||||
// --- Getters & Setters ---
|
||||
|
||||
public String getKeyword() {
|
||||
return keyword;
|
||||
}
|
||||
|
||||
public void setKeyword(String keyword) {
|
||||
this.keyword = keyword;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getCode3() {
|
||||
return code3;
|
||||
}
|
||||
|
||||
public void setCode3(String code3) {
|
||||
this.code3 = code3;
|
||||
}
|
||||
|
||||
public String getCurrency() {
|
||||
return currency;
|
||||
}
|
||||
|
||||
public void setCurrency(String currency) {
|
||||
this.currency = currency;
|
||||
}
|
||||
|
||||
// --- toString, equals & hashCode ---
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Paises{" +
|
||||
"keyword='" + keyword + '\'' +
|
||||
", code='" + code + '\'' +
|
||||
", code3='" + code3 + '\'' +
|
||||
", currency='" + currency + '\'' +
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return keyword != null ? keyword.hashCode() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (!(obj instanceof Paises other)) return false;
|
||||
return keyword != null && keyword.equals(other.keyword);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package com.imprimelibros.erp.paises;
|
||||
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/paises")
|
||||
public class PaisesController {
|
||||
|
||||
private final PaisesService paisesService;
|
||||
|
||||
public PaisesController(PaisesService paisesService) {
|
||||
this.paisesService = paisesService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compatible con Select2 (AJAX):
|
||||
* - Soporta parámetros opcionales:
|
||||
* - q / term : texto a buscar
|
||||
* - lang : ej. "es", "en" (si se omite usa Locale actual)
|
||||
*
|
||||
* Respuesta:
|
||||
* { "results": [ { "id": "espania", "text": "España" }, ... ] }
|
||||
*/
|
||||
@GetMapping
|
||||
public Map<String, Object> getPaises(
|
||||
@RequestParam(value = "q", required = false) String q1,
|
||||
@RequestParam(value = "term", required = false) String q2,
|
||||
Locale locale) {
|
||||
|
||||
|
||||
return paisesService.getForSelect(q1, q2, locale);
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package com.imprimelibros.erp.paises;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface PaisesRepository extends JpaRepository<Paises, String> {
|
||||
|
||||
Optional<Paises> findByCode(String code);
|
||||
|
||||
Optional<Paises> findByCode3(String code3);
|
||||
|
||||
Optional<Paises> findByCurrency(String currency);
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
package com.imprimelibros.erp.paises;
|
||||
|
||||
import java.text.Collator;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class PaisesService {
|
||||
|
||||
protected final PaisesRepository repo;
|
||||
protected final MessageSource messageSource;
|
||||
|
||||
public PaisesService(PaisesRepository repo, MessageSource messageSource) {
|
||||
this.repo = repo;
|
||||
this.messageSource = messageSource;
|
||||
}
|
||||
|
||||
public Map<String, Object> getForSelect(String q1, String q2, Locale locale) {
|
||||
|
||||
try {
|
||||
|
||||
// Termino de búsqueda (Select2 usa 'q' o 'term' según versión/config)
|
||||
String search = Optional.ofNullable(q1).orElse(q2);
|
||||
if (search != null) {
|
||||
search = search.trim();
|
||||
}
|
||||
final String q = (search == null || search.isEmpty())
|
||||
? null
|
||||
: search.toLowerCase(locale);
|
||||
|
||||
List<Paises> all = repo.findAll();
|
||||
|
||||
// Mapear a opciones id/text con i18n y filtrar por búsqueda si llega
|
||||
List<Map<String, String>> options = all.stream()
|
||||
.map(cc -> {
|
||||
String key = cc.getKeyword();
|
||||
String id = cc.getCode3();
|
||||
String text = messageSource.getMessage("paises." + key, null, key, locale);
|
||||
Map<String, String> m = new HashMap<>();
|
||||
m.put("id", id); // lo normal en Select2: id = valor que guardarás (code3)
|
||||
m.put("text", text); // texto mostrado, i18n con fallback a keyword
|
||||
return m;
|
||||
})
|
||||
.filter(opt -> {
|
||||
if (q == null || q.isEmpty())
|
||||
return true;
|
||||
String text = opt.get("text").toLowerCase(locale);
|
||||
String id = opt.get("id").toLowerCase(locale);
|
||||
return text.contains(q) || id.contains(q);
|
||||
})
|
||||
.sorted(Comparator.comparing(m -> m.get("text"), Collator.getInstance(locale)))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Estructura Select2
|
||||
Map<String, Object> resp = new HashMap<>();
|
||||
resp.put("results", options);
|
||||
return resp;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return Map.of("results", List.of());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -85,7 +85,6 @@ geoip.http.enabled=true
|
||||
# Hibernate Timezone
|
||||
#
|
||||
spring.jpa.properties.hibernate.jdbc.time_zone=UTC
|
||||
|
||||
#
|
||||
# PDF Templates
|
||||
#
|
||||
|
||||
@ -0,0 +1,52 @@
|
||||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: 0003-create-paises
|
||||
author: jjo
|
||||
changes:
|
||||
- createTable:
|
||||
tableName: paises
|
||||
columns:
|
||||
- column:
|
||||
name: keyword
|
||||
type: varchar(64)
|
||||
constraints:
|
||||
nullable: false
|
||||
primaryKey: true
|
||||
primaryKeyName: pk_paises
|
||||
- column:
|
||||
name: code
|
||||
type: char(2)
|
||||
constraints:
|
||||
nullable: false
|
||||
- column:
|
||||
name: code3
|
||||
type: char(3)
|
||||
constraints:
|
||||
nullable: false
|
||||
- column:
|
||||
name: currency
|
||||
type: char(3)
|
||||
constraints:
|
||||
nullable: false
|
||||
|
||||
- addUniqueConstraint:
|
||||
tableName: paises
|
||||
columnNames: code
|
||||
constraintName: uq_paises_code
|
||||
|
||||
- addUniqueConstraint:
|
||||
tableName: paises
|
||||
columnNames: code3
|
||||
constraintName: uq_paises_code3
|
||||
|
||||
- loadData:
|
||||
tableName: paises
|
||||
file: db/changelog/data/paises.csv
|
||||
separator: ","
|
||||
encoding: UTF-8
|
||||
quotchar: '"'
|
||||
|
||||
rollback:
|
||||
- dropTable:
|
||||
tableName: paises
|
||||
cascadeConstraints: true
|
||||
@ -0,0 +1,159 @@
|
||||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: 00XX-create-direcciones
|
||||
author: jjo
|
||||
changes:
|
||||
- createTable:
|
||||
tableName: direcciones
|
||||
columns:
|
||||
- column:
|
||||
name: id
|
||||
type: BIGINT AUTO_INCREMENT
|
||||
constraints:
|
||||
primaryKey: true
|
||||
nullable: false
|
||||
|
||||
- column:
|
||||
name: user_id
|
||||
type: BIGINT
|
||||
constraints:
|
||||
nullable: false
|
||||
foreignKeyName: fk_direcciones_users
|
||||
references: users(id)
|
||||
onDelete: CASCADE
|
||||
|
||||
- column:
|
||||
name: alias
|
||||
type: VARCHAR(100)
|
||||
constraints:
|
||||
nullable: false
|
||||
|
||||
- column:
|
||||
name: att
|
||||
type: VARCHAR(150)
|
||||
constraints:
|
||||
nullable: false
|
||||
|
||||
- column:
|
||||
name: direccion
|
||||
type: VARCHAR(255)
|
||||
constraints:
|
||||
nullable: false
|
||||
|
||||
- column:
|
||||
name: cp
|
||||
type: MEDIUMINT UNSIGNED
|
||||
constraints:
|
||||
nullable: false
|
||||
|
||||
- column:
|
||||
name: ciudad
|
||||
type: VARCHAR(100)
|
||||
constraints:
|
||||
nullable: false
|
||||
|
||||
- column:
|
||||
name: provincia
|
||||
type: VARCHAR(100)
|
||||
constraints:
|
||||
nullable: false
|
||||
|
||||
- column:
|
||||
name: pais_code3
|
||||
type: CHAR(3)
|
||||
constraints:
|
||||
nullable: false
|
||||
foreignKeyName: fk_direcciones_paises
|
||||
references: paises(code3)
|
||||
|
||||
- column:
|
||||
name: telefono
|
||||
type: VARCHAR(30)
|
||||
constraints:
|
||||
nullable: false
|
||||
|
||||
- column:
|
||||
name: instrucciones
|
||||
type: VARCHAR(255)
|
||||
constraints:
|
||||
nullable: true
|
||||
|
||||
- column:
|
||||
name: is_facturacion
|
||||
type: TINYINT(1)
|
||||
defaultValueNumeric: 0
|
||||
constraints:
|
||||
nullable: false
|
||||
|
||||
- column:
|
||||
name: razon_social
|
||||
type: VARCHAR(150)
|
||||
constraints:
|
||||
nullable: true
|
||||
|
||||
- column:
|
||||
name: tipo_identificacion_fiscal
|
||||
type: ENUM('DNI','NIE','CIF','Pasaporte','VAT_ID')
|
||||
defaultValue: 'DNI'
|
||||
constraints:
|
||||
nullable: false
|
||||
|
||||
- column:
|
||||
name: identificacion_fiscal
|
||||
type: VARCHAR(50)
|
||||
constraints:
|
||||
nullable: true
|
||||
|
||||
- column:
|
||||
name: created_by
|
||||
type: BIGINT
|
||||
constraints:
|
||||
nullable: true
|
||||
foreignKeyName: fk_direcciones_created
|
||||
references: users(id)
|
||||
onDelete: CASCADE
|
||||
|
||||
- column:
|
||||
name: updated_by
|
||||
type: BIGINT
|
||||
constraints:
|
||||
nullable: true
|
||||
foreignKeyName: fk_direcciones_updated
|
||||
references: users(id)
|
||||
onDelete: CASCADE
|
||||
|
||||
- column:
|
||||
name: deleted_by
|
||||
type: BIGINT
|
||||
constraints:
|
||||
nullable: true
|
||||
foreignKeyName: fk_direcciones_deleted
|
||||
references: users(id)
|
||||
onDelete: CASCADE
|
||||
|
||||
- column:
|
||||
name: deleted
|
||||
type: TINYINT(1)
|
||||
defaultValueNumeric: 0
|
||||
constraints:
|
||||
nullable: false
|
||||
|
||||
- column:
|
||||
name: created_at
|
||||
type: TIMESTAMP
|
||||
defaultValueComputed: CURRENT_TIMESTAMP
|
||||
constraints:
|
||||
nullable: false
|
||||
|
||||
- column:
|
||||
name: updated_at
|
||||
type: TIMESTAMP
|
||||
defaultValueComputed: CURRENT_TIMESTAMP
|
||||
constraints:
|
||||
nullable: false
|
||||
|
||||
- column:
|
||||
name: deleted_at
|
||||
type: TIMESTAMP
|
||||
constraints:
|
||||
nullable: true
|
||||
40
src/main/resources/db/changelog/data/paises.csv
Normal file
40
src/main/resources/db/changelog/data/paises.csv
Normal file
@ -0,0 +1,40 @@
|
||||
keyword,code,code3,currency
|
||||
argentina,ar,arg,ARS
|
||||
austria,at,aut,EUR
|
||||
australia,au,aus,AUD
|
||||
bosnia_herzegovina,ba,bih,BAM
|
||||
belgica,be,bel,EUR
|
||||
bulgaria,bg,bgr,BGN
|
||||
canada,ca,can,CAD
|
||||
suiza,ch,che,CHF
|
||||
chile,cl,chl,CLP
|
||||
colombia,co,col,COP
|
||||
republica_checa,cz,cze,CZK
|
||||
alemania,de,deu,EUR
|
||||
dinamarca,dk,dnk,DKK
|
||||
estonia,ee,est,EUR
|
||||
espania,es,esp,EUR
|
||||
finlandia,fi,fin,EUR
|
||||
francia,fr,fra,EUR
|
||||
reino_unido,gb,gbr,GBP
|
||||
grecia,gr,grc,EUR
|
||||
croacia,hr,hrv,EUR
|
||||
hungria,hu,hun,HUF
|
||||
irlanda,ie,irl,EUR
|
||||
israel,il,isr,ILS
|
||||
italia,it,ita,EUR
|
||||
lituania,lt,ltu,EUR
|
||||
luxemburgo,lu,lux,EUR
|
||||
letonia,lv,lva,EUR
|
||||
mexico,mx,mex,MXN
|
||||
holanda,nl,nld,EUR
|
||||
noruega,no,nor,NOK
|
||||
peru,pe,per,PEN
|
||||
polonia,pl,pol,PLN
|
||||
portugal,pt,prt,EUR
|
||||
rumania,ro,rou,RON
|
||||
serbia,rs,srb,RSD
|
||||
suecia,se,swe,SEK
|
||||
eslovenia,si,svn,EUR
|
||||
eslovaquia,sk,svk,EUR
|
||||
eeuu,us,usa,USD
|
||||
|
@ -3,3 +3,7 @@ databaseChangeLog:
|
||||
file: db/changelog/changesets/0001-baseline.yml
|
||||
- include:
|
||||
file: db/changelog/changesets/0002-create-pedidos.yml
|
||||
- include:
|
||||
file: db/changelog/changesets/0003-create-paises.yml
|
||||
- include:
|
||||
file: db/changelog/changesets/0004-create-direcciones.yml
|
||||
@ -5,6 +5,7 @@ app.aceptar=Aceptar
|
||||
app.cancelar=Cancelar
|
||||
app.guardar=Guardar
|
||||
app.editar=Editar
|
||||
app.add=Añadir
|
||||
app.eliminar=Eliminar
|
||||
app.imprimir=Imprimir
|
||||
app.acciones.siguiente=Siguiente
|
||||
@ -19,5 +20,7 @@ app.sidebar.inicio=Inicio
|
||||
app.sidebar.presupuestos=Presupuestos
|
||||
app.sidebar.configuracion=Configuración
|
||||
app.sidebar.usuarios=Usuarios
|
||||
app.sidebar.direcciones=Mis Direcciones
|
||||
app.sidebar.direcciones-admin=Administrar Direcciones
|
||||
|
||||
app.errors.403=No tienes permiso para acceder a esta página.
|
||||
0
src/main/resources/i18n/direcciones_en.properties
Normal file
0
src/main/resources/i18n/direcciones_en.properties
Normal file
57
src/main/resources/i18n/direcciones_es.properties
Normal file
57
src/main/resources/i18n/direcciones_es.properties
Normal file
@ -0,0 +1,57 @@
|
||||
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
|
||||
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
|
||||
direcciones.direccion=Dirección
|
||||
direcciones.cp=Código Postal
|
||||
direcciones.ciudad=Ciudad
|
||||
direcciones.provincia=Provincia
|
||||
direcciones.estado=Estado
|
||||
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
|
||||
|
||||
direcciones.tabla.id=ID
|
||||
direcciones.tabla.att=Att.
|
||||
direcciones.tabla.cliente=Cliente
|
||||
direcciones.tabla.acciones=Acciones
|
||||
|
||||
direcciones.dni=D.N.I.
|
||||
direcciones.nie=N.I.E.
|
||||
direcciones.pasaporte=Pasaporte
|
||||
direcciones.cif=C.I.F.
|
||||
direcciones.vat_id=VAT ID
|
||||
|
||||
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.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 tiene permiso para realizar esta acción.
|
||||
|
||||
direcciones.form.error.required=Campo obligatorio.
|
||||
|
||||
@ -27,5 +27,6 @@ margenes-presupuesto.delete.ok.text=El margen ha sido eliminado con éxito.
|
||||
|
||||
margenes-presupuesto.exito.eliminado=Margen eliminado con éxito.
|
||||
|
||||
margenes-presupuesto.error.noEncontrado=Margen no encontrado.
|
||||
margenes-presupuesto.error.delete-internal-error=No se puede eliminar: error interno.
|
||||
margenes-presupuesto.error.delete-not-found=No se puede eliminar: margen no encontrado.
|
||||
39
src/main/resources/i18n/paises_en.properties
Normal file
39
src/main/resources/i18n/paises_en.properties
Normal file
@ -0,0 +1,39 @@
|
||||
paises.argentina=Argentina
|
||||
paises.austria=Austria
|
||||
paises.australia=Australia
|
||||
paises.bosnia_herzegovina=Bosnia and Herzegovina
|
||||
paises.belgica=Belgium
|
||||
paises.bulgaria=Bulgaria
|
||||
paises.canada=Canada
|
||||
paises.suiza=Switzerland
|
||||
paises.chile=Chile
|
||||
paises.colombia=Colombia
|
||||
paises.republica_checa=Czech Republic
|
||||
paises.alemania=Germany
|
||||
paises.dinamarca=Denmark
|
||||
paises.estonia=Estonia
|
||||
paises.espania=Spain
|
||||
paises.finlandia=Finland
|
||||
paises.francia=France
|
||||
paises.reino_unido=United Kingdom
|
||||
paises.grecia=Greece
|
||||
paises.croacia=Croatia
|
||||
paises.hungria=Hungary
|
||||
paises.irlanda=Ireland
|
||||
paises.israel=Israel
|
||||
paises.italia=Italy
|
||||
paises.lituania=Lithuania
|
||||
paises.luxemburgo=Luxembourg
|
||||
paises.letonia=Latvia
|
||||
paises.mexico=Mexico
|
||||
paises.holanda=Netherlands
|
||||
paises.noruega=Norway
|
||||
paises.peru=Peru
|
||||
paises.polonia=Poland
|
||||
paises.portugal=Portugal
|
||||
paises.rumania=Romania
|
||||
paises.serbia=Serbia
|
||||
paises.suecia=Sweden
|
||||
paises.eslovenia=Slovenia
|
||||
paises.eslovaquia=Slovakia
|
||||
paises.eeuu=United States
|
||||
39
src/main/resources/i18n/paises_es.properties
Normal file
39
src/main/resources/i18n/paises_es.properties
Normal file
@ -0,0 +1,39 @@
|
||||
paises.argentina=Argentina
|
||||
paises.austria=Austria
|
||||
paises.australia=Australia
|
||||
paises.bosnia_herzegovina=Bosnia y Herzegovina
|
||||
paises.belgica=Bélgica
|
||||
paises.bulgaria=Bulgaria
|
||||
paises.canada=Canadá
|
||||
paises.suiza=Suiza
|
||||
paises.chile=Chile
|
||||
paises.colombia=Colombia
|
||||
paises.republica_checa=República Checa
|
||||
paises.alemania=Alemania
|
||||
paises.dinamarca=Dinamarca
|
||||
paises.estonia=Estonia
|
||||
paises.espania=España
|
||||
paises.finlandia=Finlandia
|
||||
paises.francia=Francia
|
||||
paises.reino_unido=Reino Unido
|
||||
paises.grecia=Grecia
|
||||
paises.croacia=Croacia
|
||||
paises.hungria=Hungría
|
||||
paises.irlanda=Irlanda
|
||||
paises.israel=Israel
|
||||
paises.italia=Italia
|
||||
paises.lituania=Lituania
|
||||
paises.luxemburgo=Luxemburgo
|
||||
paises.letonia=Letonia
|
||||
paises.mexico=México
|
||||
paises.holanda=Países Bajos
|
||||
paises.noruega=Noruega
|
||||
paises.peru=Perú
|
||||
paises.polonia=Polonia
|
||||
paises.portugal=Portugal
|
||||
paises.rumania=Rumanía
|
||||
paises.serbia=Serbia
|
||||
paises.suecia=Suecia
|
||||
paises.eslovenia=Eslovenia
|
||||
paises.eslovaquia=Eslovaquia
|
||||
paises.eeuu=Estados Unidos
|
||||
@ -8088,6 +8088,15 @@ a {
|
||||
color: #687cfe;
|
||||
}
|
||||
|
||||
.form-switch-custom .form-check-input:checked {
|
||||
border-color: #92b2a7;
|
||||
background-color: #cbcecd;
|
||||
}
|
||||
|
||||
.form-switch-custom .form-check-input:checked::before {
|
||||
color: #92b2a7;
|
||||
}
|
||||
|
||||
.form-switch-secondary .form-check-input:checked {
|
||||
background-color: #ff7f5d;
|
||||
border-color: #ff7f5d;
|
||||
|
||||
@ -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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
// 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"));
|
||||
|
||||
=========================================================== */
|
||||
@ -0,0 +1,222 @@
|
||||
|
||||
|
||||
(() => {
|
||||
// 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 table = new DataTable('#direcciones-datatable', {
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
orderCellsTop: true,
|
||||
pageLength: 50,
|
||||
language: { url: '/assets/libs/datatables/i18n/' + language + '.json' },
|
||||
responsive: true,
|
||||
dom: $('#isUser').val() == 1 ? 'lrtip' : 'lrBtip',
|
||||
buttons: {
|
||||
dom: {
|
||||
button: {
|
||||
className: 'btn btn-sm btn-outline-primary me-1'
|
||||
},
|
||||
buttons: [
|
||||
{ extend: 'copy' },
|
||||
{ extend: 'csv' },
|
||||
{ extend: 'excel' },
|
||||
{ extend: 'pdf' },
|
||||
{ extend: 'print' },
|
||||
{ extend: 'colvis' }
|
||||
],
|
||||
}
|
||||
},
|
||||
ajax: {
|
||||
url: '/direcciones/datatable',
|
||||
method: 'GET',
|
||||
},
|
||||
order: [[0, 'asc']],
|
||||
columns: [
|
||||
{ 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: 'att', name: 'att', orderable: true },
|
||||
{ data: 'direccion', name: 'direccion', orderable: true },
|
||||
{ data: 'cp', name: 'cp', orderable: true },
|
||||
{ data: 'ciudad', name: 'ciudad', orderable: true },
|
||||
{ data: 'provincia', name: 'provincia', orderable: true },
|
||||
{ data: 'pais', name: 'pais', orderable: true },
|
||||
{ data: 'actions', name: 'actions' }
|
||||
],
|
||||
columnDefs: [{ targets: -1, orderable: false, searchable: false }]
|
||||
});
|
||||
|
||||
table.on("keyup", ".direcciones-filter", function () {
|
||||
const colName = $(this).data("col");
|
||||
const colIndex = table.settings()[0].aoColumns.findIndex(c => c.name === colName);
|
||||
|
||||
if (colIndex >= 0) {
|
||||
table.column(colIndex).search(normalizeNumericFilter(this.value)).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();
|
||||
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('/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-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',
|
||||
},
|
||||
});
|
||||
$('#direccion-datatable').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();
|
||||
table.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);
|
||||
initSelect2Cliente(true);
|
||||
return;
|
||||
}
|
||||
// Fallback
|
||||
$('#direccionFormModalBody').html('<div class="p-3 text-danger">Error inesperado.</div>');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
})();
|
||||
@ -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>');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
})();
|
||||
@ -0,0 +1,180 @@
|
||||
<div th:fragment="direccionForm">
|
||||
<form id="direccionForm" novalidate th:action="${action}" th:object="${dirForm}" method="post"
|
||||
th:data-add="#{direcciones.add}" th:data-edit="#{direcciones.editar}">
|
||||
|
||||
<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}" />
|
||||
|
||||
<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
|
||||
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 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
|
||||
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 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;"
|
||||
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 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 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">
|
||||
<label for="ciudad">
|
||||
<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
|
||||
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 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 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">
|
||||
<label for="pais">
|
||||
<span th:text="#{direcciones.pais}">País</span>
|
||||
<span class="text-danger">*</span>
|
||||
</label>
|
||||
<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} == ${dirForm.paisCode3}">
|
||||
</option>
|
||||
</select>
|
||||
<div class="invalid-feedback" th:if="${#fields.hasErrors('paisCode3')}" th:errors="*{paisCode3}"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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"
|
||||
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 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"
|
||||
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" 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="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"
|
||||
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 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: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" 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">
|
||||
<label for="identificacionFiscal">
|
||||
<span th:text="#{direcciones.identificacion_fiscal}">Número de identificación fiscal</span>
|
||||
<span class="text-danger">*</span>
|
||||
</label>
|
||||
<input class="form-control direccion-item" id="identificacionFiscal" th:field="*{identificacionFiscal}"
|
||||
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.save}"></button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
@ -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>
|
||||
@ -0,0 +1,128 @@
|
||||
<!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}" />
|
||||
|
||||
<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>
|
||||
|
||||
<table id="direcciones-datatable" class="table table-striped table-nowrap responsive w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<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.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>
|
||||
<th scope="col" th:text="#{direcciones.provincia}">Provincia</th>
|
||||
<th scope="col" th:text="#{direcciones.pais}">País</th>
|
||||
<th scope="col" th:text="#{direcciones.tabla.acciones}">Acciones</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><input type="text" class="form-control form-control-sm direcciones-filter"
|
||||
data-col="id" /></th>
|
||||
<th>
|
||||
<input type="text" class="form-control form-control-sm direcciones-filter"
|
||||
data-col="cliente" />
|
||||
</th>
|
||||
<th>
|
||||
<input type="text" class="form-control form-control-sm direcciones-filter"
|
||||
data-col="alias" />
|
||||
</th>
|
||||
<th>
|
||||
<input type="nombre" class="form-control form-control-sm direcciones-filter"
|
||||
data-col="margenMin" />
|
||||
</th>
|
||||
<th>
|
||||
<input type="direccion" class="form-control form-control-sm direcciones-filter"
|
||||
data-col="margenMax" />
|
||||
</th>
|
||||
<th>
|
||||
<input type="cp" class="form-control form-control-sm direcciones-filter"
|
||||
data-col="cp" />
|
||||
</th>
|
||||
<th>
|
||||
<input type="ciudad" class="form-control form-control-sm direcciones-filter"
|
||||
data-col="ciudad" />
|
||||
</th>
|
||||
<th>
|
||||
<input type="provincia" class="form-control form-control-sm direcciones-filter"
|
||||
data-col="provincia" />
|
||||
</th>
|
||||
<th>
|
||||
<input type="pais" class="form-control form-control-sm direcciones-filter"
|
||||
data-col="pais" />
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</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 type="module" th:src="@{/assets/js/pages/imprimelibros/direcciones/list.js}"></script>
|
||||
|
||||
</th:block>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -43,6 +43,16 @@
|
||||
<i class="ri-file-paper-2-line"></i> <span th:text="#{app.sidebar.presupuestos}">Presupuestos</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link menu-link" href="/direcciones">
|
||||
<i class="ri-truck-line"></i>
|
||||
<span th:if="${#authentication.principal.role == 'SUPERADMIN' or #authentication.principal.role == 'ADMIN'}"
|
||||
th:text="#{app.sidebar.direcciones-admin}">Administrar Direcciones</span>
|
||||
</span>
|
||||
<span th:if="${#authentication.principal.role != 'SUPERADMIN' and #authentication.principal.role != 'ADMIN'}"
|
||||
th:text="#{app.sidebar.direcciones}">Mis Direcciones</span>
|
||||
</a>
|
||||
</li>
|
||||
<li th:if="${#authentication.principal.role == 'SUPERADMIN' or #authentication.principal.role == 'ADMIN'}" class="nav-item">
|
||||
<a class="nav-link menu-link collapsed" href="#sidebarConfig" data-bs-toggle="collapse"
|
||||
role="button" aria-expanded="false" aria-controls="sidebarConfig">
|
||||
|
||||
Reference in New Issue
Block a user