mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-01-22 16:50:21 +00:00
Merge branch 'feat/checkout' into 'main'
Feat/checkout See merge request jjimenez/erp-imprimelibros!19
This commit is contained in:
37
pom.xml
37
pom.xml
@ -6,7 +6,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-parent</artifactId>
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
<version>3.5.6</version>
|
<version>3.5.7</version>
|
||||||
<relativePath /> <!-- lookup parent from repository -->
|
<relativePath /> <!-- lookup parent from repository -->
|
||||||
</parent>
|
</parent>
|
||||||
<groupId>com.imprimelibros</groupId>
|
<groupId>com.imprimelibros</groupId>
|
||||||
@ -151,6 +151,41 @@
|
|||||||
<version>${liquibase.version}</version>
|
<version>${liquibase.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Redsys -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>sis.redsys</groupId>
|
||||||
|
<artifactId>apiSha256</artifactId>
|
||||||
|
<version>1.0</version>
|
||||||
|
<scope>system</scope>
|
||||||
|
<systemPath>${project.basedir}/src/main/resources/lib/apiSha256.jar</systemPath>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Dependencias locales incluidas en el ZIP -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bouncycastle</groupId>
|
||||||
|
<artifactId>bcprov-jdk15on</artifactId>
|
||||||
|
<version>1.47</version>
|
||||||
|
<scope>system</scope>
|
||||||
|
<systemPath>${project.basedir}/src/main/resources/lib/bcprov-jdk15on-1.4.7.jar</systemPath>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>commons-codec</groupId>
|
||||||
|
<artifactId>commons-codec</artifactId>
|
||||||
|
<version>1.3</version>
|
||||||
|
<scope>system</scope>
|
||||||
|
<systemPath>${project.basedir}/src/main/resources/lib/commons-codec-1.3.jar</systemPath>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.json</groupId>
|
||||||
|
<artifactId>json</artifactId>
|
||||||
|
<version>1.0</version>
|
||||||
|
<scope>system</scope>
|
||||||
|
<systemPath>${project.basedir}/src/main/resources/lib/org.json.jar</systemPath>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@ -1,18 +1,23 @@
|
|||||||
package com.imprimelibros.erp.cart;
|
package com.imprimelibros.erp.cart;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "carts",
|
@Table(name = "carts", uniqueConstraints = @UniqueConstraint(name = "uq_carts_user_active", columnNames = { "user_id",
|
||||||
uniqueConstraints = @UniqueConstraint(name="uq_carts_user_active", columnNames={"user_id","status"}))
|
"status" }))
|
||||||
public class Cart {
|
public class Cart {
|
||||||
|
|
||||||
public enum Status { ACTIVE, LOCKED, ABANDONED }
|
public enum Status {
|
||||||
|
ACTIVE, LOCKED, ABANDONED
|
||||||
|
}
|
||||||
|
|
||||||
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
@Column(name = "user_id", nullable = false)
|
@Column(name = "user_id", nullable = false)
|
||||||
@ -25,6 +30,9 @@ public class Cart {
|
|||||||
@Column(nullable = false, length = 3)
|
@Column(nullable = false, length = 3)
|
||||||
private String currency = "EUR";
|
private String currency = "EUR";
|
||||||
|
|
||||||
|
@Column(name = "only_one_shipment", nullable = false)
|
||||||
|
private Boolean onlyOneShipment = true;
|
||||||
|
|
||||||
@Column(name = "created_at", nullable = false)
|
@Column(name = "created_at", nullable = false)
|
||||||
private LocalDateTime createdAt = LocalDateTime.now();
|
private LocalDateTime createdAt = LocalDateTime.now();
|
||||||
|
|
||||||
@ -34,23 +42,93 @@ public class Cart {
|
|||||||
@OneToMany(mappedBy = "cart", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
|
@OneToMany(mappedBy = "cart", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
|
||||||
private List<CartItem> items = new ArrayList<>();
|
private List<CartItem> items = new ArrayList<>();
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "cart", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
|
||||||
|
private List<CartDireccion> direcciones = new ArrayList<>();
|
||||||
|
|
||||||
|
@Column(name = "total", nullable = false)
|
||||||
|
private BigDecimal total = BigDecimal.ZERO;
|
||||||
|
|
||||||
@PreUpdate
|
@PreUpdate
|
||||||
public void preUpdate() { this.updatedAt = LocalDateTime.now(); }
|
public void preUpdate() {
|
||||||
|
this.updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
// Getters & Setters
|
// Getters & Setters
|
||||||
public Long getId() { return id; }
|
public Long getId() {
|
||||||
public Long getUserId() { return userId; }
|
return id;
|
||||||
public void setUserId(Long userId) { this.userId = userId; }
|
}
|
||||||
|
|
||||||
public Status getStatus() { return status; }
|
public Long getUserId() {
|
||||||
public void setStatus(Status status) { this.status = status; }
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
public String getCurrency() { return currency; }
|
public void setUserId(Long userId) {
|
||||||
public void setCurrency(String currency) { this.currency = currency; }
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
public Status getStatus() {
|
||||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
public List<CartItem> getItems() { return items; }
|
public void setStatus(Status status) {
|
||||||
public void setItems(List<CartItem> items) { this.items = items; }
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCurrency() {
|
||||||
|
return currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCurrency(String currency) {
|
||||||
|
this.currency = currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getOnlyOneShipment() {
|
||||||
|
return onlyOneShipment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnlyOneShipment(Boolean onlyOneShipment) {
|
||||||
|
this.onlyOneShipment = onlyOneShipment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getTotal() {
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotal(BigDecimal total) {
|
||||||
|
this.total = total;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getUpdatedAt() {
|
||||||
|
return updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CartItem> getItems() {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setItems(List<CartItem> items) {
|
||||||
|
this.items = items;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CartDireccion> getDirecciones() {
|
||||||
|
return direcciones;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDirecciones(List<CartDireccion> direcciones) {
|
||||||
|
this.direcciones = direcciones;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addDireccion(CartDireccion d) {
|
||||||
|
direcciones.add(d);
|
||||||
|
d.setCart(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeDireccion(CartDireccion d) {
|
||||||
|
direcciones.remove(d);
|
||||||
|
d.setCart(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,71 +3,105 @@ package com.imprimelibros.erp.cart;
|
|||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
import com.imprimelibros.erp.users.UserDetailsImpl;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
import com.imprimelibros.erp.users.User;
|
import org.springframework.context.MessageSource;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.core.Authentication;
|
import com.imprimelibros.erp.common.Utils;
|
||||||
|
import com.imprimelibros.erp.direcciones.Direccion;
|
||||||
|
import com.imprimelibros.erp.direcciones.DireccionService;
|
||||||
|
import com.imprimelibros.erp.i18n.TranslationService;
|
||||||
|
|
||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import com.imprimelibros.erp.cart.dto.UpdateCartRequest;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@RequestMapping("/cart")
|
@RequestMapping("/cart")
|
||||||
public class CartController {
|
public class CartController {
|
||||||
|
|
||||||
private final CartService service;
|
protected final CartService service;
|
||||||
|
protected DireccionService direccionService;
|
||||||
|
protected MessageSource messageSource;
|
||||||
|
protected TranslationService translationService;
|
||||||
|
|
||||||
public CartController(CartService service) {
|
public CartController(CartService service, DireccionService direccionService, MessageSource messageSource,
|
||||||
|
TranslationService translationService) {
|
||||||
this.service = service;
|
this.service = service;
|
||||||
}
|
this.direccionService = direccionService;
|
||||||
|
this.messageSource = messageSource;
|
||||||
/**
|
this.translationService = translationService;
|
||||||
* Obtiene el ID de usuario desde tu seguridad.
|
|
||||||
* Adáptalo a tu UserDetails (e.g., SecurityContext con getId())
|
|
||||||
*/
|
|
||||||
private Long currentUserId(Principal principal) {
|
|
||||||
if (principal == null) {
|
|
||||||
throw new IllegalStateException("Usuario no autenticado");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (principal instanceof Authentication auth) {
|
|
||||||
Object principalObj = auth.getPrincipal();
|
|
||||||
|
|
||||||
if (principalObj instanceof UserDetailsImpl udi) {
|
|
||||||
return udi.getId();
|
|
||||||
} else if (principalObj instanceof User u && u.getId() != null) {
|
|
||||||
return u.getId();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new IllegalStateException("No se pudo obtener el ID del usuario actual");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Vista del carrito */
|
/** Vista del carrito */
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public String viewCart(Model model, Principal principal, Locale locale) {
|
public String viewCart(Model model, Principal principal, Locale locale) {
|
||||||
var items = service.listItems(currentUserId(principal), locale);
|
|
||||||
|
List<String> keys = List.of(
|
||||||
|
"app.cancelar",
|
||||||
|
"app.seleccionar",
|
||||||
|
"cart.shipping.add.title",
|
||||||
|
"cart.shipping.select-placeholder",
|
||||||
|
"cart.shipping.new-address",
|
||||||
|
"cart.shipping.errors.noAddressSelected",
|
||||||
|
"cart.shipping.enter-units",
|
||||||
|
"cart.shipping.units-label",
|
||||||
|
"cart.shipping.errors.units-error",
|
||||||
|
"cart.shipping.ud",
|
||||||
|
"cart.shipping.uds",
|
||||||
|
"cart.shipping.send-in-palets",
|
||||||
|
"cart.shipping.send-in-palets.info",
|
||||||
|
"cart.shipping.tipo-envio",
|
||||||
|
"cart.pass-to.customer.error",
|
||||||
|
"cart.pass-to.customer.error-move",
|
||||||
|
"app.yes",
|
||||||
|
"app.aceptar",
|
||||||
|
"app.cancelar");
|
||||||
|
|
||||||
|
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||||
|
model.addAttribute("languageBundle", translations);
|
||||||
|
|
||||||
|
Long userId = Utils.currentUserId(principal);
|
||||||
|
Cart cart = service.getOrCreateActiveCart(userId);
|
||||||
|
|
||||||
|
var items = service.listItems(userId, locale);
|
||||||
model.addAttribute("items", items);
|
model.addAttribute("items", items);
|
||||||
|
|
||||||
|
Map<String, Object> direcciones = service.getCartDirecciones(cart.getId(), locale);
|
||||||
|
if (direcciones != null && direcciones.containsKey("mainDir"))
|
||||||
|
model.addAttribute("mainDir", direcciones.get("mainDir"));
|
||||||
|
else if (direcciones != null && direcciones.containsKey("direcciones"))
|
||||||
|
model.addAttribute("direcciones", direcciones.get("direcciones"));
|
||||||
|
|
||||||
|
var summary = service.getCartSummary(cart, locale);
|
||||||
|
model.addAttribute("cartSummary", summary);
|
||||||
|
if (summary.get("errorShipmentCost") != null && (Boolean) summary.get("errorShipmentCost"))
|
||||||
|
model.addAttribute("errorEnvio", true);
|
||||||
|
else
|
||||||
|
model.addAttribute("errorEnvio", false);
|
||||||
|
|
||||||
|
model.addAttribute("cart", cart);
|
||||||
return "imprimelibros/cart/cart"; // crea esta vista si quieres (tabla simple)
|
return "imprimelibros/cart/cart"; // crea esta vista si quieres (tabla simple)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Añadir presupuesto via POST form */
|
/** Añadir presupuesto via POST form */
|
||||||
@PostMapping("/add")
|
@PostMapping("/add")
|
||||||
public String add(@PathVariable(name = "presupuestoId", required = true) Long presupuestoId, Principal principal) {
|
public String add(@PathVariable(name = "presupuestoId", required = true) Long presupuestoId, Principal principal) {
|
||||||
service.addPresupuesto(currentUserId(principal), presupuestoId);
|
service.addPresupuesto(Utils.currentUserId(principal), presupuestoId);
|
||||||
return "redirect:/cart";
|
return "redirect:/cart";
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Añadir presupuesto con ruta REST (opcional) */
|
/** Añadir presupuesto con ruta REST (opcional) */
|
||||||
@PostMapping("/add/{presupuestoId}")
|
@PostMapping("/add/{presupuestoId}")
|
||||||
public Object addPath(@PathVariable Long presupuestoId, Principal principal, HttpServletRequest request) {
|
public Object addPath(@PathVariable Long presupuestoId, Principal principal, HttpServletRequest request) {
|
||||||
service.addPresupuesto(currentUserId(principal), presupuestoId);
|
service.addPresupuesto(Utils.currentUserId(principal), presupuestoId);
|
||||||
boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
|
boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
|
||||||
if (isAjax) {
|
if (isAjax) {
|
||||||
// Responder 200 con la URL a la que quieres ir
|
// Responder 200 con la URL a la que quieres ir
|
||||||
@ -83,13 +117,13 @@ public class CartController {
|
|||||||
public long getCount(Principal principal) {
|
public long getCount(Principal principal) {
|
||||||
if (principal == null)
|
if (principal == null)
|
||||||
return 0;
|
return 0;
|
||||||
return service.countItems(currentUserId(principal));
|
return service.countItems(Utils.currentUserId(principal));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Eliminar línea por ID de item */
|
/** Eliminar línea por ID de item */
|
||||||
@DeleteMapping("/{itemId}/remove")
|
@DeleteMapping("/{itemId}/remove")
|
||||||
public String remove(@PathVariable Long itemId, Principal principal) {
|
public String remove(@PathVariable Long itemId, Principal principal) {
|
||||||
service.removeItem(currentUserId(principal), itemId);
|
service.removeItem(Utils.currentUserId(principal), itemId);
|
||||||
return "redirect:/cart";
|
return "redirect:/cart";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,14 +131,72 @@ public class CartController {
|
|||||||
@DeleteMapping("/delete/item/{presupuestoId}")
|
@DeleteMapping("/delete/item/{presupuestoId}")
|
||||||
@ResponseBody
|
@ResponseBody
|
||||||
public String removeByPresupuesto(@PathVariable Long presupuestoId, Principal principal) {
|
public String removeByPresupuesto(@PathVariable Long presupuestoId, Principal principal) {
|
||||||
service.removeByPresupuesto(currentUserId(principal), presupuestoId);
|
service.removeByPresupuesto(Utils.currentUserId(principal), presupuestoId);
|
||||||
return "redirect:/cart";
|
return "redirect:/cart";
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Vaciar carrito completo */
|
/** Vaciar carrito completo */
|
||||||
@DeleteMapping("/clear")
|
@DeleteMapping("/clear")
|
||||||
public String clear(Principal principal) {
|
public String clear(Principal principal) {
|
||||||
service.clear(currentUserId(principal));
|
service.clear(Utils.currentUserId(principal));
|
||||||
return "redirect:/cart";
|
return "redirect:/cart";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/get-address/{id}")
|
||||||
|
public String getDireccionCard(@PathVariable Long id, @RequestParam(required = false) Long presupuestoId,
|
||||||
|
@RequestParam(required = false) Integer unidades,
|
||||||
|
@RequestParam(required = false) Integer isPalets,
|
||||||
|
Model model, Locale locale) {
|
||||||
|
Direccion dir = direccionService.findById(id)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
model.addAttribute("pais", messageSource.getMessage("paises." + dir.getPais().getKeyword(), null,
|
||||||
|
dir.getPais().getKeyword(), locale));
|
||||||
|
model.addAttribute("presupuestoId", presupuestoId);
|
||||||
|
model.addAttribute("unidades", unidades);
|
||||||
|
model.addAttribute("isPalets", isPalets);
|
||||||
|
model.addAttribute("direccion", dir);
|
||||||
|
|
||||||
|
return "imprimelibros/direcciones/direccionCard :: direccionCard(direccion=${direccion})";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "/update/{id}", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||||
|
public String updateCart(@PathVariable Long id, UpdateCartRequest updateRequest, Model model, Locale locale,
|
||||||
|
Principal principal) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
service.updateCart(id, updateRequest);
|
||||||
|
var cartSummary = service.getCartSummary(service.getCartById(id), locale);
|
||||||
|
model.addAttribute("cartSummary", cartSummary);
|
||||||
|
|
||||||
|
return "imprimelibros/cart/_cartSummary :: cartSummary(summary=${cartSummary})";
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
|
||||||
|
// redirect to cart with error message
|
||||||
|
String errorMessage = messageSource.getMessage("cart.update.error", null, "Error updating cart", locale);
|
||||||
|
model.addAttribute("errorMessage", errorMessage);
|
||||||
|
return "redirect:/cart";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "/pass-to-customer/{customerId}")
|
||||||
|
public ResponseEntity<?> moveToCustomer(
|
||||||
|
@PathVariable Long customerId,
|
||||||
|
Principal principal) {
|
||||||
|
|
||||||
|
if(!Utils.isCurrentUserAdmin()) {
|
||||||
|
return ResponseEntity.status(403).body(Map.of("error", "Forbidden"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Long userId = Utils.currentUserId(principal);
|
||||||
|
Cart cart = service.getOrCreateActiveCart(userId);
|
||||||
|
|
||||||
|
boolean ok = service.moveCartToCustomer(cart.getId(), customerId);
|
||||||
|
|
||||||
|
if (ok)
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
return ResponseEntity.status(400).body(Map.of("error", "cart.errors.move-cart"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
101
src/main/java/com/imprimelibros/erp/cart/CartDireccion.java
Normal file
101
src/main/java/com/imprimelibros/erp/cart/CartDireccion.java
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package com.imprimelibros.erp.cart;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import org.springframework.context.MessageSource;
|
||||||
|
import com.imprimelibros.erp.cart.dto.DireccionCardDTO;
|
||||||
|
import com.imprimelibros.erp.direcciones.Direccion;
|
||||||
|
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "cart_direcciones")
|
||||||
|
public class CartDireccion {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "cart_id", nullable = false)
|
||||||
|
private Cart cart;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "direccion_id", nullable = false)
|
||||||
|
private Direccion direccion;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "presupuesto_id")
|
||||||
|
private Presupuesto presupuesto;
|
||||||
|
|
||||||
|
@Column(name = "unidades")
|
||||||
|
private Integer unidades;
|
||||||
|
|
||||||
|
@Column(name = "isPalets", nullable = false)
|
||||||
|
private Boolean isPalets;
|
||||||
|
|
||||||
|
// --- Getters & Setters ---
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Cart getCart() {
|
||||||
|
return cart;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCart(Cart cart) {
|
||||||
|
this.cart = cart;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Direccion getDireccion() {
|
||||||
|
return direccion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDireccion(Direccion direccion) {
|
||||||
|
this.direccion = direccion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Presupuesto getPresupuesto() {
|
||||||
|
return presupuesto;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPresupuesto(Presupuesto presupuesto) {
|
||||||
|
this.presupuesto = presupuesto;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getUnidades() {
|
||||||
|
return unidades;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUnidades(Integer unidades) {
|
||||||
|
this.unidades = unidades;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getIsPalets() {
|
||||||
|
return isPalets;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsPalets(Boolean isPalets) {
|
||||||
|
this.isPalets = isPalets;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DireccionCardDTO toDireccionCard(MessageSource messageSource, Locale locale) {
|
||||||
|
|
||||||
|
String pais = messageSource.getMessage("paises." + this.direccion.getPais().getKeyword(), null,
|
||||||
|
this.direccion.getPais().getKeyword(), locale);
|
||||||
|
|
||||||
|
return new DireccionCardDTO(
|
||||||
|
this.direccion,
|
||||||
|
this.presupuesto != null ? this.presupuesto.getId() : null,
|
||||||
|
this.unidades,
|
||||||
|
this.isPalets,
|
||||||
|
pais
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -3,6 +3,8 @@ package com.imprimelibros.erp.cart;
|
|||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(
|
@Table(
|
||||||
name = "cart_items",
|
name = "cart_items",
|
||||||
@ -17,8 +19,9 @@ public class CartItem {
|
|||||||
@JoinColumn(name = "cart_id", nullable = false)
|
@JoinColumn(name = "cart_id", nullable = false)
|
||||||
private Cart cart;
|
private Cart cart;
|
||||||
|
|
||||||
@Column(name = "presupuesto_id", nullable = false)
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
private Long presupuestoId;
|
@JoinColumn(name = "presupuesto_id", nullable = false)
|
||||||
|
private Presupuesto presupuesto;
|
||||||
|
|
||||||
@Column(name = "created_at", nullable = false)
|
@Column(name = "created_at", nullable = false)
|
||||||
private LocalDateTime createdAt = LocalDateTime.now();
|
private LocalDateTime createdAt = LocalDateTime.now();
|
||||||
@ -29,8 +32,8 @@ public class CartItem {
|
|||||||
public Cart getCart() { return cart; }
|
public Cart getCart() { return cart; }
|
||||||
public void setCart(Cart cart) { this.cart = cart; }
|
public void setCart(Cart cart) { this.cart = cart; }
|
||||||
|
|
||||||
public Long getPresupuestoId() { return presupuestoId; }
|
public Presupuesto getPresupuesto() { return presupuesto; }
|
||||||
public void setPresupuestoId(Long presupuestoId) { this.presupuestoId = presupuestoId; }
|
public void setPresupuesto(Presupuesto presupuesto) { this.presupuesto = presupuesto; }
|
||||||
|
|
||||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,22 @@
|
|||||||
package com.imprimelibros.erp.cart;
|
package com.imprimelibros.erp.cart;
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface CartRepository extends JpaRepository<Cart, Long> {
|
public interface CartRepository extends JpaRepository<Cart, Long> {
|
||||||
Optional<Cart> findByUserIdAndStatus(Long userId, Cart.Status status);
|
Optional<Cart> findByUserIdAndStatus(Long userId, Cart.Status status);
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
select distinct c from Cart c
|
||||||
|
left join fetch c.direcciones cd
|
||||||
|
left join fetch cd.direccion d
|
||||||
|
left join fetch d.pais p
|
||||||
|
left join fetch cd.presupuesto pr
|
||||||
|
where c.id = :id
|
||||||
|
""")
|
||||||
|
Optional<Cart> findByIdFetchAll(@Param("id") Long id);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package com.imprimelibros.erp.cart;
|
package com.imprimelibros.erp.cart;
|
||||||
|
|
||||||
import jakarta.transaction.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import org.springframework.context.MessageSource;
|
import org.springframework.context.MessageSource;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@ -10,32 +10,43 @@ import java.util.HashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
import com.imprimelibros.erp.presupuesto.classes.PresupuestoFormatter;
|
import com.imprimelibros.erp.presupuesto.classes.PresupuestoFormatter;
|
||||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||||
|
import com.imprimelibros.erp.cart.dto.DireccionCardDTO;
|
||||||
|
import com.imprimelibros.erp.cart.dto.DireccionShipment;
|
||||||
|
import com.imprimelibros.erp.cart.dto.UpdateCartRequest;
|
||||||
import com.imprimelibros.erp.common.Utils;
|
import com.imprimelibros.erp.common.Utils;
|
||||||
|
import com.imprimelibros.erp.direcciones.DireccionService;
|
||||||
|
import com.imprimelibros.erp.externalApi.skApiClient;
|
||||||
|
import com.imprimelibros.erp.pedido.PedidoService;
|
||||||
import com.imprimelibros.erp.presupuesto.PresupuestoRepository;
|
import com.imprimelibros.erp.presupuesto.PresupuestoRepository;
|
||||||
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class CartService {
|
public class CartService {
|
||||||
|
|
||||||
private final CartRepository cartRepo;
|
private final CartRepository cartRepo;
|
||||||
private final CartItemRepository itemRepo;
|
private final CartItemRepository itemRepo;
|
||||||
private final MessageSource messageSource;
|
private final MessageSource messageSource;
|
||||||
private final PresupuestoFormatter presupuestoFormatter;
|
|
||||||
private final PresupuestoRepository presupuestoRepo;
|
private final PresupuestoRepository presupuestoRepo;
|
||||||
private final Utils utils;
|
private final Utils utils;
|
||||||
|
private final DireccionService direccionService;
|
||||||
|
private final skApiClient skApiClient;
|
||||||
|
private final PedidoService pedidoService;
|
||||||
|
|
||||||
public CartService(CartRepository cartRepo, CartItemRepository itemRepo,
|
public CartService(CartRepository cartRepo, CartItemRepository itemRepo,
|
||||||
MessageSource messageSource, PresupuestoFormatter presupuestoFormatter,
|
MessageSource messageSource, PresupuestoFormatter presupuestoFormatter,
|
||||||
PresupuestoRepository presupuestoRepo, Utils utils) {
|
PresupuestoRepository presupuestoRepo, Utils utils, DireccionService direccionService,
|
||||||
|
skApiClient skApiClient, PedidoService pedidoService) {
|
||||||
this.cartRepo = cartRepo;
|
this.cartRepo = cartRepo;
|
||||||
this.itemRepo = itemRepo;
|
this.itemRepo = itemRepo;
|
||||||
this.messageSource = messageSource;
|
this.messageSource = messageSource;
|
||||||
this.presupuestoFormatter = presupuestoFormatter;
|
|
||||||
this.presupuestoRepo = presupuestoRepo;
|
this.presupuestoRepo = presupuestoRepo;
|
||||||
this.utils = utils;
|
this.utils = utils;
|
||||||
|
this.direccionService = direccionService;
|
||||||
|
this.skApiClient = skApiClient;
|
||||||
|
this.pedidoService = pedidoService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Devuelve el carrito activo o lo crea si no existe. */
|
/** Devuelve el carrito activo o lo crea si no existe. */
|
||||||
@ -50,6 +61,11 @@ public class CartService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Cart getCartById(Long cartId) {
|
||||||
|
return cartRepo.findById(cartId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Carrito no encontrado"));
|
||||||
|
}
|
||||||
|
|
||||||
/** Lista items (presupuestos) del carrito activo del usuario. */
|
/** Lista items (presupuestos) del carrito activo del usuario. */
|
||||||
@Transactional
|
@Transactional
|
||||||
public List<Map<String, Object>> listItems(Long userId, Locale locale) {
|
public List<Map<String, Object>> listItems(Long userId, Locale locale) {
|
||||||
@ -58,14 +74,13 @@ public class CartService {
|
|||||||
List<CartItem> items = itemRepo.findByCartId(cart.getId());
|
List<CartItem> items = itemRepo.findByCartId(cart.getId());
|
||||||
for (CartItem item : items) {
|
for (CartItem item : items) {
|
||||||
|
|
||||||
Presupuesto p = presupuestoRepo.findById(item.getPresupuestoId())
|
Presupuesto p = item.getPresupuesto();
|
||||||
.orElseThrow(() -> new IllegalStateException("Presupuesto no encontrado: " + item.getPresupuestoId()));
|
|
||||||
|
|
||||||
Map<String, Object> elemento = getElementoCart(p, locale);
|
Map<String, Object> elemento = getElementoCart(p, locale);
|
||||||
elemento.put("cartItemId", item.getId());
|
elemento.put("cartItemId", item.getId());
|
||||||
resultados.add(elemento);
|
resultados.add(elemento);
|
||||||
}
|
}
|
||||||
//System.out.println("Cart items: " + resultados);
|
// System.out.println("Cart items: " + resultados);
|
||||||
return resultados;
|
return resultados;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,7 +92,8 @@ public class CartService {
|
|||||||
if (!exists) {
|
if (!exists) {
|
||||||
CartItem ci = new CartItem();
|
CartItem ci = new CartItem();
|
||||||
ci.setCart(cart);
|
ci.setCart(cart);
|
||||||
ci.setPresupuestoId(presupuestoId);
|
ci.setPresupuesto(presupuestoRepo.findById(presupuestoId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Presupuesto no encontrado")));
|
||||||
itemRepo.save(ci);
|
itemRepo.save(ci);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -96,8 +112,9 @@ public class CartService {
|
|||||||
@Transactional
|
@Transactional
|
||||||
public void removeByPresupuesto(Long userId, Long presupuestoId) {
|
public void removeByPresupuesto(Long userId, Long presupuestoId) {
|
||||||
Cart cart = getOrCreateActiveCart(userId);
|
Cart cart = getOrCreateActiveCart(userId);
|
||||||
itemRepo.findByCartIdAndPresupuestoId(cart.getId(), presupuestoId)
|
CartItem item = itemRepo.findByCartIdAndPresupuestoId(cart.getId(), presupuestoId)
|
||||||
.ifPresent(itemRepo::delete);
|
.orElseThrow(() -> new IllegalArgumentException("Item no encontrado"));
|
||||||
|
itemRepo.deleteById(item.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Vacía todo el carrito activo. */
|
/** Vacía todo el carrito activo. */
|
||||||
@ -122,9 +139,9 @@ public class CartService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, Object> getElementoCart(Presupuesto presupuesto, Locale locale) {
|
private Map<String, Object> getElementoCart(Presupuesto presupuesto, Locale locale) {
|
||||||
|
|
||||||
Map<String, Object> resumen = new HashMap<>();
|
Map<String, Object> resumen = new HashMap<>();
|
||||||
|
|
||||||
resumen.put("titulo", presupuesto.getTitulo());
|
resumen.put("titulo", presupuesto.getTitulo());
|
||||||
|
|
||||||
resumen.put("imagen",
|
resumen.put("imagen",
|
||||||
@ -134,8 +151,15 @@ public class CartService {
|
|||||||
|
|
||||||
resumen.put("presupuestoId", presupuesto.getId());
|
resumen.put("presupuestoId", presupuesto.getId());
|
||||||
|
|
||||||
|
if (presupuesto.getServiciosJson() != null && presupuesto.getServiciosJson().contains("ejemplar-prueba")) {
|
||||||
|
resumen.put("hasSample", true);
|
||||||
|
} else {
|
||||||
|
resumen.put("hasSample", false);
|
||||||
|
}
|
||||||
Map<String, Object> detalles = utils.getTextoPresupuesto(presupuesto, locale);
|
Map<String, Object> detalles = utils.getTextoPresupuesto(presupuesto, locale);
|
||||||
|
|
||||||
|
resumen.put("tirada", presupuesto.getSelectedTirada());
|
||||||
|
|
||||||
resumen.put("baseTotal", Utils.formatCurrency(presupuesto.getBaseImponible(), locale));
|
resumen.put("baseTotal", Utils.formatCurrency(presupuesto.getBaseImponible(), locale));
|
||||||
resumen.put("base", presupuesto.getBaseImponible());
|
resumen.put("base", presupuesto.getBaseImponible());
|
||||||
resumen.put("iva4", presupuesto.getIvaImporte4());
|
resumen.put("iva4", presupuesto.getIvaImporte4());
|
||||||
@ -145,4 +169,257 @@ public class CartService {
|
|||||||
|
|
||||||
return resumen;
|
return resumen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getCartSummary(Cart cart, Locale locale) {
|
||||||
|
|
||||||
|
double base = 0.0;
|
||||||
|
double iva4 = 0.0;
|
||||||
|
double iva21 = 0.0;
|
||||||
|
double shipment = 0.0;
|
||||||
|
|
||||||
|
Boolean errorShipementCost = false;
|
||||||
|
|
||||||
|
List<CartItem> items = cart.getItems();
|
||||||
|
List<CartDireccion> direcciones = cart.getDirecciones();
|
||||||
|
|
||||||
|
for (CartItem item : items) {
|
||||||
|
Presupuesto p = item.getPresupuesto();
|
||||||
|
Double peso = p.getPeso() != null ? p.getPeso().doubleValue() : 0.0;
|
||||||
|
base += p.getBaseImponible().doubleValue();
|
||||||
|
iva4 += p.getIvaImporte4().doubleValue();
|
||||||
|
iva21 += p.getIvaImporte21().doubleValue();
|
||||||
|
if (cart.getOnlyOneShipment() != null && cart.getOnlyOneShipment()) {
|
||||||
|
// Si es envío único, que es a españa y no ha canarias
|
||||||
|
if (direcciones != null && direcciones.size() > 0) {
|
||||||
|
CartDireccion cd = direcciones.get(0);
|
||||||
|
Boolean freeShipment = direccionService.checkFreeShipment(cd.getDireccion().getCp(),
|
||||||
|
cd.getDireccion().getPaisCode3()) && !cd.getIsPalets();
|
||||||
|
if (!freeShipment) {
|
||||||
|
Integer unidades = p.getSelectedTirada();
|
||||||
|
Map<String, Object> res = getShippingCost(cd, peso, unidades, locale);
|
||||||
|
if (res.get("success").equals(Boolean.FALSE)) {
|
||||||
|
errorShipementCost = true;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
shipment += (Double) res.get("shipment");
|
||||||
|
iva21 += (Double) res.get("iva21");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// si tiene prueba de envio, hay que añadir el coste
|
||||||
|
if (p.getServiciosJson() != null && p.getServiciosJson().contains("ejemplar-prueba")) {
|
||||||
|
|
||||||
|
Map<String, Object> res = getShippingCost(cd, peso, 1, locale);
|
||||||
|
if (res.get("success").equals(Boolean.FALSE)) {
|
||||||
|
errorShipementCost = true;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
shipment += (Double) res.get("shipment");
|
||||||
|
iva21 += (Double) res.get("iva21");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// envio por cada presupuesto
|
||||||
|
// buscar la direccion asignada a este presupuesto
|
||||||
|
if (direcciones == null)
|
||||||
|
continue;
|
||||||
|
List<CartDireccion> cd_presupuesto = direcciones.stream()
|
||||||
|
.filter(d -> d.getPresupuesto() != null && d.getPresupuesto().getId().equals(p.getId())
|
||||||
|
&& d.getUnidades() != null && d.getUnidades() != null && d.getUnidades() > 0)
|
||||||
|
.toList();
|
||||||
|
Boolean firstDirection = true;
|
||||||
|
for (CartDireccion cd : cd_presupuesto) {
|
||||||
|
Integer unidades = cd.getUnidades();
|
||||||
|
if (firstDirection) {
|
||||||
|
Boolean freeShipment = direccionService.checkFreeShipment(cd.getDireccion().getCp(),
|
||||||
|
cd.getDireccion().getPaisCode3()) && !cd.getIsPalets();
|
||||||
|
if (!freeShipment && unidades != null && unidades > 0) {
|
||||||
|
Map<String, Object> res = getShippingCost(cd, peso, unidades, locale);
|
||||||
|
if (res.get("success").equals(Boolean.FALSE)) {
|
||||||
|
errorShipementCost = true;
|
||||||
|
} else {
|
||||||
|
shipment += (Double) res.get("shipment");
|
||||||
|
iva21 += (Double) res.get("iva21");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
firstDirection = false;
|
||||||
|
} else {
|
||||||
|
Map<String, Object> res = getShippingCost(cd, peso, unidades, locale);
|
||||||
|
if (res.get("success").equals(Boolean.FALSE)) {
|
||||||
|
errorShipementCost = true;
|
||||||
|
} else {
|
||||||
|
shipment += (Double) res.get("shipment");
|
||||||
|
iva21 += (Double) res.get("iva21");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ejemplar de prueba
|
||||||
|
CartDireccion cd_prueba = direcciones.stream()
|
||||||
|
.filter(d -> d.getPresupuesto() != null && d.getPresupuesto().getId().equals(p.getId())
|
||||||
|
&& d.getUnidades() == null)
|
||||||
|
.findFirst().orElse(null);
|
||||||
|
if (cd_prueba != null) {
|
||||||
|
|
||||||
|
Map<String, Object> res = getShippingCost(cd_prueba, peso, 1, locale);
|
||||||
|
if (res.get("success").equals(Boolean.FALSE)) {
|
||||||
|
errorShipementCost = true;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
shipment += (Double) res.get("shipment");
|
||||||
|
iva21 += (Double) res.get("iva21");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double total = base + iva4 + iva21 + shipment;
|
||||||
|
|
||||||
|
int fidelizacion = pedidoService.getDescuentoFidelizacion();
|
||||||
|
double descuento = (total) * fidelizacion / 100.0;
|
||||||
|
total -= descuento;
|
||||||
|
|
||||||
|
Map<String, Object> summary = new HashMap<>();
|
||||||
|
summary.put("base", Utils.formatCurrency(base, locale));
|
||||||
|
summary.put("iva4", Utils.formatCurrency(iva4, locale));
|
||||||
|
summary.put("iva21", Utils.formatCurrency(iva21, locale));
|
||||||
|
summary.put("shipment", Utils.formatCurrency(shipment, locale));
|
||||||
|
summary.put("fidelizacion", fidelizacion + "%");
|
||||||
|
summary.put("descuento", Utils.formatCurrency(-descuento, locale));
|
||||||
|
summary.put("total", Utils.formatCurrency(total, locale));
|
||||||
|
summary.put("errorShipmentCost", errorShipementCost);
|
||||||
|
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public Map<String, Object> getCartDirecciones(Long cartId, Locale locale) {
|
||||||
|
Cart cart = cartRepo.findByIdFetchAll(cartId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Carrito no encontrado"));
|
||||||
|
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
List<CartDireccion> direcciones = cart.getDirecciones();
|
||||||
|
|
||||||
|
if (cart.getOnlyOneShipment() && !direcciones.isEmpty()) {
|
||||||
|
result.put("mainDir", direcciones.get(0).toDireccionCard(messageSource, locale));
|
||||||
|
} else {
|
||||||
|
List<DireccionCardDTO> dirCards = cart.getDirecciones().stream()
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.map(cd -> cd.toDireccionCard(messageSource, locale))
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.toList();
|
||||||
|
result.put("direcciones", dirCards);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Boolean updateCart(Long cartId, UpdateCartRequest request) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
Cart cart = cartRepo.findById(cartId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Carrito no encontrado"));
|
||||||
|
cart.setOnlyOneShipment(request.isOnlyOneShipment());
|
||||||
|
// Borramos todas las direcciones actuales de la bbdd
|
||||||
|
// Opcional (limpieza): romper backref antes de clear
|
||||||
|
for (CartDireccion d : cart.getDirecciones()) {
|
||||||
|
d.setCart(null);
|
||||||
|
}
|
||||||
|
cart.getDirecciones().clear();
|
||||||
|
// Guardamos las direcciones
|
||||||
|
List<DireccionShipment> direcciones = request.getDirecciones();
|
||||||
|
if (direcciones != null && direcciones.size() > 0) {
|
||||||
|
for (DireccionShipment dir : direcciones) {
|
||||||
|
// Crear una nueva CartDireccion por cada item
|
||||||
|
CartDireccion cd = new CartDireccion();
|
||||||
|
cd.setCart(cart);
|
||||||
|
cd.setDireccion(dir.getId() != null ? direccionService.findById(dir.getId())
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Dirección no encontrada")) : null);
|
||||||
|
cd.setIsPalets(dir.getIsPalets() != null ? dir.getIsPalets() : false);
|
||||||
|
cd.setUnidades(dir.getUnidades() != null ? dir.getUnidades() : null);
|
||||||
|
if (dir.getPresupuestoId() != null) {
|
||||||
|
Presupuesto p = presupuestoRepo.findById(dir.getPresupuestoId())
|
||||||
|
.orElse(null);
|
||||||
|
cd.setPresupuesto(p);
|
||||||
|
}
|
||||||
|
cart.addDireccion(cd);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
}
|
||||||
|
cartRepo.save(cart);
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Manejo de excepciones
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean moveCartToCustomer(Long cartId, Long customerId) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Remove the cart from the customer if they have one
|
||||||
|
Cart existingCart = cartRepo.findByUserIdAndStatus(customerId, Cart.Status.ACTIVE)
|
||||||
|
.orElse(null);
|
||||||
|
if (existingCart != null) {
|
||||||
|
cartRepo.delete(existingCart);
|
||||||
|
}
|
||||||
|
|
||||||
|
Cart cart = cartRepo.findById(cartId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Carrito no encontrado"));
|
||||||
|
|
||||||
|
cart.setUserId(customerId);
|
||||||
|
cartRepo.save(cart);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Manejo de excepciones
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/***************************************
|
||||||
|
* MÉTODOS PRIVADOS
|
||||||
|
***************************************/
|
||||||
|
|
||||||
|
private Map<String, Object> getShippingCost(
|
||||||
|
CartDireccion cd,
|
||||||
|
Double peso,
|
||||||
|
Integer unidades,
|
||||||
|
Locale locale) {
|
||||||
|
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Map<String, Object> data = Map.of(
|
||||||
|
"cp", cd.getDireccion().getCp(),
|
||||||
|
"pais_code3", cd.getDireccion().getPaisCode3(),
|
||||||
|
"peso", peso != null ? peso : 0.0,
|
||||||
|
"unidades", unidades,
|
||||||
|
"palets", Boolean.TRUE.equals(cd.getIsPalets()) ? 1 : 0);
|
||||||
|
|
||||||
|
var shipmentCost = skApiClient.getCosteEnvio(data, locale);
|
||||||
|
|
||||||
|
if (shipmentCost != null && shipmentCost.get("data") != null) {
|
||||||
|
Number n = (Number) shipmentCost.get("data");
|
||||||
|
double cost = n.doubleValue();
|
||||||
|
|
||||||
|
result.put("success", true);
|
||||||
|
result.put("shipment", cost);
|
||||||
|
result.put("iva21", cost * 0.21);
|
||||||
|
} else {
|
||||||
|
result.put("success", false);
|
||||||
|
result.put("shipment", 0.0);
|
||||||
|
result.put("iva21", 0.0);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.put("success", false);
|
||||||
|
result.put("shipment", 0.0);
|
||||||
|
result.put("iva21", 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,15 @@
|
|||||||
|
package com.imprimelibros.erp.cart.dto;
|
||||||
|
|
||||||
|
import com.imprimelibros.erp.cart.CartDireccion;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface CartDireccionRepository extends JpaRepository<CartDireccion, Long> {
|
||||||
|
|
||||||
|
// Borrado masivo por cart_id
|
||||||
|
void deleteByCartId(Long cartId);
|
||||||
|
|
||||||
|
// Lectura por cart_id (útil para componer respuestas)
|
||||||
|
List<CartDireccion> findByCartId(Long cartId);
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
package com.imprimelibros.erp.cart.dto;
|
||||||
|
|
||||||
|
import com.imprimelibros.erp.direcciones.Direccion;
|
||||||
|
|
||||||
|
public class DireccionCardDTO {
|
||||||
|
private final Direccion direccion;
|
||||||
|
private final Long presupuestoId;
|
||||||
|
private final Integer unidades;
|
||||||
|
private final Boolean isPalets;
|
||||||
|
private final String pais;
|
||||||
|
|
||||||
|
public DireccionCardDTO(Direccion direccion, Long presupuestoId, Integer unidades, Boolean isPalets, String pais) {
|
||||||
|
this.direccion = direccion;
|
||||||
|
this.presupuestoId = presupuestoId;
|
||||||
|
this.unidades = unidades;
|
||||||
|
this.isPalets = isPalets;
|
||||||
|
this.pais = pais;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Direccion getDireccion() {
|
||||||
|
return direccion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getPresupuestoId() {
|
||||||
|
return presupuestoId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getUnidades() {
|
||||||
|
return unidades;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getIsPalets() {
|
||||||
|
return isPalets;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPais() {
|
||||||
|
return pais;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
package com.imprimelibros.erp.cart.dto;
|
||||||
|
|
||||||
|
public class DireccionShipment {
|
||||||
|
|
||||||
|
private Long id; // puede no venir → null
|
||||||
|
private String cp; // puede no venir → null
|
||||||
|
private String paisCode3; // puede no venir → null
|
||||||
|
private Long presupuestoId; // puede no venir → null
|
||||||
|
private Integer unidades; // puede no venir → null
|
||||||
|
private Boolean isPalets; // puede no venir → null
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCp() {
|
||||||
|
return cp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCp(String cp) {
|
||||||
|
this.cp = cp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPaisCode3() {
|
||||||
|
return paisCode3;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPaisCode3(String paisCode3) {
|
||||||
|
this.paisCode3 = paisCode3;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getPresupuestoId() {
|
||||||
|
return presupuestoId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPresupuestoId(Long presupuestoId) {
|
||||||
|
this.presupuestoId = presupuestoId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getUnidades() {
|
||||||
|
return unidades;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUnidades(Integer unidades) {
|
||||||
|
this.unidades = unidades;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getIsPalets() {
|
||||||
|
return isPalets;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsPalets(Boolean isPalets) {
|
||||||
|
this.isPalets = isPalets;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
package com.imprimelibros.erp.cart.dto;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class UpdateCartRequest {
|
||||||
|
private Boolean onlyOneShipment = Boolean.FALSE; // default: false
|
||||||
|
private List<DireccionShipment> direcciones = new ArrayList<>();
|
||||||
|
|
||||||
|
public boolean isOnlyOneShipment() { // boolean-style getter
|
||||||
|
return Boolean.TRUE.equals(onlyOneShipment);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnlyOneShipment(Boolean onlyOneShipment) {
|
||||||
|
this.onlyOneShipment = onlyOneShipment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DireccionShipment> getDirecciones() {
|
||||||
|
return direcciones;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDirecciones(List<DireccionShipment> direcciones) {
|
||||||
|
this.direcciones = (direcciones != null) ? direcciones : new ArrayList<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
package com.imprimelibros.erp.checkout;
|
||||||
|
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.context.MessageSource;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
|
||||||
|
import com.imprimelibros.erp.common.Utils;
|
||||||
|
import com.imprimelibros.erp.i18n.TranslationService;
|
||||||
|
import com.imprimelibros.erp.paises.PaisesService;
|
||||||
|
|
||||||
|
import com.imprimelibros.erp.direcciones.DireccionService;
|
||||||
|
|
||||||
|
import com.imprimelibros.erp.cart.CartService;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/checkout")
|
||||||
|
public class CheckoutController {
|
||||||
|
|
||||||
|
protected CartService cartService;
|
||||||
|
protected TranslationService translationService;
|
||||||
|
protected PaisesService paisesService;
|
||||||
|
protected DireccionService direccionService;
|
||||||
|
protected MessageSource messageSource;
|
||||||
|
|
||||||
|
public CheckoutController(CartService cartService, TranslationService translationService,
|
||||||
|
PaisesService paisesService, DireccionService direccionService, MessageSource messageSource) {
|
||||||
|
this.cartService = cartService;
|
||||||
|
this.translationService = translationService;
|
||||||
|
this.paisesService = paisesService;
|
||||||
|
this.direccionService = direccionService;
|
||||||
|
this.messageSource = messageSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public String view(Model model, Principal principal, Locale locale) {
|
||||||
|
|
||||||
|
List<String> keys = List.of(
|
||||||
|
"app.cancelar",
|
||||||
|
"app.seleccionar",
|
||||||
|
"checkout.shipping.add.title",
|
||||||
|
"checkout.shipping.select-placeholder",
|
||||||
|
"checkout.shipping.new-address",
|
||||||
|
"app.yes",
|
||||||
|
"app.cancelar");
|
||||||
|
|
||||||
|
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||||
|
model.addAttribute("languageBundle", translations);
|
||||||
|
|
||||||
|
var items = this.cartService.listItems(Utils.currentUserId(principal), locale);
|
||||||
|
for (var item : items) {
|
||||||
|
if (item.get("hasSample") != null && (Boolean) item.get("hasSample")) {
|
||||||
|
model.addAttribute("hasSample", true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
model.addAttribute("items", items);
|
||||||
|
return "imprimelibros/checkout/checkout"; // crea esta vista si quieres (tabla simple)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ package com.imprimelibros.erp.common;
|
|||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
|
import java.security.Principal;
|
||||||
import java.text.NumberFormat;
|
import java.text.NumberFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@ -12,6 +13,8 @@ import java.util.Optional;
|
|||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
|
|
||||||
import org.springframework.context.MessageSource;
|
import org.springframework.context.MessageSource;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
@ -22,6 +25,8 @@ import com.imprimelibros.erp.presupuesto.classes.PresupuestoFormatter;
|
|||||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||||
import com.imprimelibros.erp.presupuesto.maquetacion.MaquetacionMatrices;
|
import com.imprimelibros.erp.presupuesto.maquetacion.MaquetacionMatrices;
|
||||||
import com.imprimelibros.erp.presupuesto.marcapaginas.Marcapaginas;
|
import com.imprimelibros.erp.presupuesto.marcapaginas.Marcapaginas;
|
||||||
|
import com.imprimelibros.erp.users.User;
|
||||||
|
import com.imprimelibros.erp.users.UserDetailsImpl;
|
||||||
|
|
||||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||||
import jakarta.persistence.criteria.Path;
|
import jakarta.persistence.criteria.Path;
|
||||||
@ -40,6 +45,30 @@ public class Utils {
|
|||||||
this.messageSource = messageSource;
|
this.messageSource = messageSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isCurrentUserAdmin() {
|
||||||
|
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
return auth.getAuthorities().stream()
|
||||||
|
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN") || a.getAuthority().equals("ROLE_SUPERADMIN"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Long currentUserId(Principal principal) {
|
||||||
|
|
||||||
|
if (principal == null) {
|
||||||
|
throw new IllegalStateException("Usuario no autenticado");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (principal instanceof Authentication auth) {
|
||||||
|
Object principalObj = auth.getPrincipal();
|
||||||
|
|
||||||
|
if (principalObj instanceof UserDetailsImpl udi) {
|
||||||
|
return udi.getId();
|
||||||
|
} else if (principalObj instanceof User u && u.getId() != null) {
|
||||||
|
return u.getId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("No se pudo obtener el ID del usuario actual");
|
||||||
|
}
|
||||||
|
|
||||||
public static String formatCurrency(BigDecimal amount, Locale locale) {
|
public static String formatCurrency(BigDecimal amount, Locale locale) {
|
||||||
NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance(locale);
|
NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance(locale);
|
||||||
return currencyFormatter.format(amount);
|
return currencyFormatter.format(amount);
|
||||||
|
|||||||
@ -91,7 +91,6 @@ public class SecurityConfig {
|
|||||||
// Ignora CSRF para tu recurso público (sin Ant/Mvc matchers)
|
// Ignora CSRF para tu recurso público (sin Ant/Mvc matchers)
|
||||||
.csrf(csrf -> csrf
|
.csrf(csrf -> csrf
|
||||||
.ignoringRequestMatchers(pathStartsWith("/presupuesto/public/")))
|
.ignoringRequestMatchers(pathStartsWith("/presupuesto/public/")))
|
||||||
|
|
||||||
// ====== RequestCache: sólo navegaciones HTML reales ======
|
// ====== RequestCache: sólo navegaciones HTML reales ======
|
||||||
.requestCache(rc -> {
|
.requestCache(rc -> {
|
||||||
HttpSessionRequestCache cache = new HttpSessionRequestCache();
|
HttpSessionRequestCache cache = new HttpSessionRequestCache();
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package com.imprimelibros.erp.direcciones;
|
package com.imprimelibros.erp.direcciones;
|
||||||
|
|
||||||
|
import java.security.Principal;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -43,20 +44,23 @@ import jakarta.validation.Valid;
|
|||||||
@RequestMapping("/direcciones")
|
@RequestMapping("/direcciones")
|
||||||
public class DireccionController {
|
public class DireccionController {
|
||||||
|
|
||||||
|
private final DireccionService direccionService;
|
||||||
|
|
||||||
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 UserDao userRepo;
|
||||||
protected final TranslationService translationService;
|
protected final TranslationService translationService;
|
||||||
|
|
||||||
|
|
||||||
public DireccionController(DireccionRepository repo, PaisesService paisesService,
|
public DireccionController(DireccionRepository repo, PaisesService paisesService,
|
||||||
MessageSource messageSource, UserDao userRepo, TranslationService translationService) {
|
MessageSource messageSource, UserDao userRepo, TranslationService translationService,
|
||||||
|
DireccionService direccionService) {
|
||||||
this.repo = repo;
|
this.repo = repo;
|
||||||
this.paisesService = paisesService;
|
this.paisesService = paisesService;
|
||||||
this.messageSource = messageSource;
|
this.messageSource = messageSource;
|
||||||
this.userRepo = userRepo;
|
this.userRepo = userRepo;
|
||||||
this.translationService = translationService;
|
this.translationService = translationService;
|
||||||
|
this.direccionService = direccionService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping()
|
@GetMapping()
|
||||||
@ -295,6 +299,33 @@ public class DireccionController {
|
|||||||
return "imprimelibros/direcciones/direccion-form :: direccionForm";
|
return "imprimelibros/direcciones/direccion-form :: direccionForm";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("direction-form")
|
||||||
|
public String getForm(@RequestParam(required = false) Long id,
|
||||||
|
Direccion direccion,
|
||||||
|
BindingResult binding,
|
||||||
|
Model model,
|
||||||
|
HttpServletResponse response,
|
||||||
|
Principal principal,
|
||||||
|
Locale locale) {
|
||||||
|
|
||||||
|
model.addAttribute("paises", paisesService.getForSelect("", "", locale).get("results"));
|
||||||
|
|
||||||
|
Direccion newDireccion = new Direccion();
|
||||||
|
|
||||||
|
User user = null;
|
||||||
|
if (principal instanceof UserDetailsImpl udi) {
|
||||||
|
user = new User();
|
||||||
|
user.setId(udi.getId());
|
||||||
|
} else if (principal instanceof User u && u.getId() != null) {
|
||||||
|
user = u;
|
||||||
|
}
|
||||||
|
newDireccion.setUser(user);
|
||||||
|
model.addAttribute("dirForm", newDireccion);
|
||||||
|
model.addAttribute("action", "/direcciones/add");
|
||||||
|
|
||||||
|
return "imprimelibros/direcciones/direccion-form-fixed-user :: direccionForm";
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public String create(
|
public String create(
|
||||||
@Valid @ModelAttribute("dirForm") Direccion direccion,
|
@Valid @ModelAttribute("dirForm") Direccion direccion,
|
||||||
@ -327,6 +358,34 @@ public class DireccionController {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// para el formulario modal en checkout
|
||||||
|
@PostMapping("/add")
|
||||||
|
public String create2(
|
||||||
|
@Valid @ModelAttribute("dirForm") Direccion direccion,
|
||||||
|
BindingResult binding,
|
||||||
|
Model model,
|
||||||
|
HttpServletResponse response,
|
||||||
|
Authentication auth,
|
||||||
|
Locale locale) {
|
||||||
|
|
||||||
|
User current = userRepo.findByUserNameIgnoreCaseAndEnabledTrueAndDeletedFalse(auth.getName()).orElse(null);
|
||||||
|
direccion.setUser(current);
|
||||||
|
|
||||||
|
if (binding.hasErrors()) {
|
||||||
|
response.setStatus(422);
|
||||||
|
model.addAttribute("paises", paisesService.getForSelect("", "", locale).get("results"));
|
||||||
|
model.addAttribute("action", "/direcciones/add");
|
||||||
|
model.addAttribute("dirForm", direccion);
|
||||||
|
return "imprimelibros/direcciones/direccion-form-fixed-user :: direccionForm";
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = direccion;
|
||||||
|
|
||||||
|
repo.save(data);
|
||||||
|
response.setStatus(201);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/{id}")
|
@PostMapping("/{id}")
|
||||||
public String update(
|
public String update(
|
||||||
@PathVariable Long id,
|
@PathVariable Long id,
|
||||||
@ -416,12 +475,36 @@ public class DireccionController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping(value = "/select2", produces = "application/json")
|
||||||
|
@ResponseBody
|
||||||
|
public Map<String, Object> getSelect2(
|
||||||
|
@RequestParam(value = "q", required = false) String q1,
|
||||||
|
@RequestParam(value = "term", required = false) String q2,
|
||||||
|
@RequestParam(value = "presupuestoId", required = false) Long presupuestoId,
|
||||||
|
Authentication auth) {
|
||||||
|
|
||||||
|
boolean isAdmin = auth.getAuthorities().stream()
|
||||||
|
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN") || a.getAuthority().equals("ROLE_SUPERADMIN"));
|
||||||
|
|
||||||
|
Long currentUserId = null;
|
||||||
|
if (!isAdmin) {
|
||||||
|
if (auth != null && auth.getPrincipal() instanceof UserDetailsImpl udi) {
|
||||||
|
currentUserId = udi.getId();
|
||||||
|
} else if (auth != null) {
|
||||||
|
currentUserId = userRepo.findIdByUserNameIgnoreCase(auth.getName()).orElse(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return direccionService.getForSelect(q1, q2, isAdmin ? null : currentUserId);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isOwnerOrAdmin(Authentication auth, Long ownerId) {
|
private boolean isOwnerOrAdmin(Authentication auth, Long ownerId) {
|
||||||
if (auth == null) {
|
if (auth == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
boolean isAdmin = auth.getAuthorities().stream()
|
boolean isAdmin = auth.getAuthorities().stream()
|
||||||
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
|
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN") || a.getAuthority().equals("ROLE_SUPERADMIN"));
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -434,4 +517,5 @@ public class DireccionController {
|
|||||||
}
|
}
|
||||||
return currentUserId != null && currentUserId.equals(ownerId);
|
return currentUserId != null && currentUserId.equals(ownerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,97 @@
|
|||||||
|
package com.imprimelibros.erp.direcciones;
|
||||||
|
|
||||||
|
import java.text.Collator;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class DireccionService {
|
||||||
|
|
||||||
|
protected DireccionRepository repo;
|
||||||
|
|
||||||
|
public DireccionService(DireccionRepository repo) {
|
||||||
|
this.repo = repo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getForSelect(String q1, String q2, Long userId) {
|
||||||
|
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();
|
||||||
|
|
||||||
|
List<Direccion> all = userId != null ? repo.findByUserId(userId) : 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 id = cc.getId().toString();
|
||||||
|
String alias = cc.getAlias();
|
||||||
|
String direccion = cc.getDireccion();
|
||||||
|
String cp = String.valueOf(cc.getCp());
|
||||||
|
String ciudad = cc.getCiudad();
|
||||||
|
String att = cc.getAtt();
|
||||||
|
Map<String, String> m = new HashMap<>();
|
||||||
|
m.put("id", id); // lo normal en Select2: id = valor que guardarás (code3)
|
||||||
|
m.put("text", alias); // texto mostrado, i18n con fallback a keyword
|
||||||
|
m.put("cp", cp);
|
||||||
|
m.put("ciudad", ciudad);
|
||||||
|
m.put("att", att);
|
||||||
|
m.put("alias", alias);
|
||||||
|
m.put("direccion", direccion);
|
||||||
|
return m;
|
||||||
|
})
|
||||||
|
.filter(opt -> {
|
||||||
|
if (q == null || q.isEmpty())
|
||||||
|
return true;
|
||||||
|
String cp = opt.get("cp");
|
||||||
|
String ciudad = opt.get("ciudad").toLowerCase();
|
||||||
|
String att = opt.get("att").toLowerCase();
|
||||||
|
String alias = opt.get("alias").toLowerCase();
|
||||||
|
String text = opt.get("text").toLowerCase();
|
||||||
|
String direccion = opt.get("direccion").toLowerCase();
|
||||||
|
return text.contains(q) || cp.contains(q) || ciudad.contains(q) || att.contains(q)
|
||||||
|
|| alias.contains(q) || direccion.contains(q);
|
||||||
|
})
|
||||||
|
.sorted(Comparator.comparing(m -> m.get("text"), Collator.getInstance()))
|
||||||
|
.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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Direccion> findById(Long id) {
|
||||||
|
return repo.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean checkFreeShipment(Integer cp, String paisCode3) {
|
||||||
|
if (paisCode3 != null && paisCode3.toLowerCase().equals("esp") && cp != null) {
|
||||||
|
// Excluir Canarias (35xxx y 38xxx), Baleares (07xxx), Ceuta (51xxx), Melilla
|
||||||
|
// (52xxx)
|
||||||
|
int provincia = cp / 1000;
|
||||||
|
|
||||||
|
if (provincia != 7 && provincia != 35 && provincia != 38 && provincia != 51 && provincia != 52) {
|
||||||
|
return true; // España peninsular
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ import org.springframework.http.*;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.client.HttpClientErrorException;
|
import org.springframework.web.client.HttpClientErrorException;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
@ -18,8 +19,10 @@ import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoCubierta;
|
|||||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoEncuadernacion;
|
import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoEncuadernacion;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
|
import java.net.URI;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
@ -219,6 +222,54 @@ public class skApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getCosteEnvio(Map<String, Object> data, Locale locale) {
|
||||||
|
|
||||||
|
return performWithRetryMap(() -> {
|
||||||
|
String url = this.skApiUrl + "api/calcular-envio";
|
||||||
|
|
||||||
|
URI uri = UriComponentsBuilder.fromUriString(url)
|
||||||
|
.queryParam("pais_code3", data.get("pais_code3"))
|
||||||
|
.queryParam("cp", data.get("cp"))
|
||||||
|
.queryParam("peso", data.get("peso"))
|
||||||
|
.queryParam("unidades", data.get("unidades"))
|
||||||
|
.queryParam("palets", data.get("palets"))
|
||||||
|
.build(true) // no re-encode []
|
||||||
|
.toUri();
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setBearerAuth(authService.getToken());
|
||||||
|
|
||||||
|
|
||||||
|
ResponseEntity<String> response = restTemplate.exchange(
|
||||||
|
uri,
|
||||||
|
HttpMethod.GET,
|
||||||
|
new HttpEntity<>(headers),
|
||||||
|
String.class);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Map<String, Object> responseBody = new ObjectMapper().readValue(
|
||||||
|
response.getBody(),
|
||||||
|
new TypeReference<Map<String, Object>>() {
|
||||||
|
});
|
||||||
|
Boolean error = (Boolean) responseBody.get("error");
|
||||||
|
if (error != null && error) {
|
||||||
|
return Map.of("error", messageSource.getMessage("direcciones.error.noShippingCost", null, locale));
|
||||||
|
} else {
|
||||||
|
Double total = Optional.ofNullable(responseBody.get("data"))
|
||||||
|
.filter(Number.class::isInstance)
|
||||||
|
.map(Number.class::cast)
|
||||||
|
.map(Number::doubleValue)
|
||||||
|
.orElse(0.0);
|
||||||
|
|
||||||
|
return Map.of("data", total);
|
||||||
|
}
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return Map.of("error", "Internal Server Error: 1"); // Fallback en caso de error
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/******************
|
/******************
|
||||||
* PRIVATE METHODS
|
* PRIVATE METHODS
|
||||||
******************/
|
******************/
|
||||||
@ -236,6 +287,20 @@ public class skApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> performWithRetryMap(Supplier<Map<String, Object>> request) {
|
||||||
|
try {
|
||||||
|
return request.get();
|
||||||
|
} catch (HttpClientErrorException.Unauthorized e) {
|
||||||
|
// Token expirado, renovar y reintentar
|
||||||
|
authService.invalidateToken();
|
||||||
|
try {
|
||||||
|
return request.get(); // segundo intento
|
||||||
|
} catch (HttpClientErrorException ex) {
|
||||||
|
throw new RuntimeException("La autenticación ha fallado tras renovar el token.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static BigDecimal calcularMargen(
|
private static BigDecimal calcularMargen(
|
||||||
BigDecimal importe, BigDecimal importeMin, BigDecimal importeMax,
|
BigDecimal importe, BigDecimal importeMin, BigDecimal importeMax,
|
||||||
BigDecimal margenMax, BigDecimal margenMin) {
|
BigDecimal margenMax, BigDecimal margenMin) {
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
package com.imprimelibros.erp.pdf;
|
package com.imprimelibros.erp.pdf;
|
||||||
|
|
||||||
import com.openhtmltopdf.outputdevice.helper.BaseRendererBuilder;
|
import com.openhtmltopdf.outputdevice.helper.BaseRendererBuilder;
|
||||||
import com.openhtmltopdf.pdfboxout.PdfRendererBuilder;
|
|
||||||
import org.springframework.core.io.Resource;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,26 @@
|
|||||||
|
package com.imprimelibros.erp.pedido;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class PedidoService {
|
||||||
|
|
||||||
|
public int getDescuentoFidelizacion() {
|
||||||
|
// descuento entre el 1% y el 6% para clientes fidelidad (mas de 1500€ en el ultimo año)
|
||||||
|
double totalGastado = 1600.0; // Ejemplo, deberías obtenerlo del historial del cliente
|
||||||
|
if(totalGastado < 1200) {
|
||||||
|
return 0;
|
||||||
|
} else if(totalGastado >= 1200 && totalGastado < 1999) {
|
||||||
|
return 1;
|
||||||
|
} else if(totalGastado >= 2000 && totalGastado < 2999) {
|
||||||
|
return 2;
|
||||||
|
} else if(totalGastado >= 3000 && totalGastado < 3999) {
|
||||||
|
return 3;
|
||||||
|
} else if(totalGastado >= 4000 && totalGastado < 4999) {
|
||||||
|
return 4;
|
||||||
|
} else if(totalGastado >= 5000) {
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ import org.springframework.stereotype.Controller;
|
|||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
|
|
||||||
|
import java.security.Principal;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@ -621,14 +622,14 @@ public class PresupuestoController {
|
|||||||
@ResponseBody
|
@ResponseBody
|
||||||
public DataTablesResponse<Map<String, Object>> datatable(
|
public DataTablesResponse<Map<String, Object>> datatable(
|
||||||
HttpServletRequest request, Authentication auth, Locale locale,
|
HttpServletRequest request, Authentication auth, Locale locale,
|
||||||
@PathVariable("tipo") String tipo) {
|
@PathVariable("tipo") String tipo, Principal principal) {
|
||||||
|
|
||||||
DataTablesRequest dt = DataTablesParser.from(request);
|
DataTablesRequest dt = DataTablesParser.from(request);
|
||||||
|
|
||||||
if ("anonimos".equals(tipo)) {
|
if ("anonimos".equals(tipo)) {
|
||||||
return dtService.datatablePublicos(dt, locale);
|
return dtService.datatablePublicos(dt, locale, principal);
|
||||||
} else if ("clientes".equals(tipo)) {
|
} else if ("clientes".equals(tipo)) {
|
||||||
return dtService.datatablePrivados(dt, locale);
|
return dtService.datatablePrivados(dt, locale, principal);
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException("Tipo de datatable no válido");
|
throw new IllegalArgumentException("Tipo de datatable no válido");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,18 @@
|
|||||||
package com.imprimelibros.erp.presupuesto;
|
package com.imprimelibros.erp.presupuesto;
|
||||||
|
|
||||||
import com.imprimelibros.erp.common.Utils;
|
import com.imprimelibros.erp.common.Utils;
|
||||||
|
import com.imprimelibros.erp.configuracion.margenes_presupuestos.MargenPresupuesto;
|
||||||
import com.imprimelibros.erp.datatables.*;
|
import com.imprimelibros.erp.datatables.*;
|
||||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||||
|
|
||||||
import jakarta.persistence.criteria.Expression;
|
import jakarta.persistence.criteria.Expression;
|
||||||
|
|
||||||
import org.springframework.context.MessageSource;
|
import org.springframework.context.MessageSource;
|
||||||
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.security.Principal;
|
||||||
import java.time.*;
|
import java.time.*;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@ -26,18 +29,29 @@ public class PresupuestoDatatableService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public DataTablesResponse<Map<String, Object>> datatablePublicos(DataTablesRequest dt, Locale locale) {
|
public DataTablesResponse<Map<String, Object>> datatablePublicos(DataTablesRequest dt, Locale locale,
|
||||||
return commonDataTable(dt, locale, "publico", true);
|
Principal principal) {
|
||||||
|
return commonDataTable(dt, locale, "publico", true, principal);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public DataTablesResponse<Map<String, Object>> datatablePrivados(DataTablesRequest dt, Locale locale) {
|
public DataTablesResponse<Map<String, Object>> datatablePrivados(DataTablesRequest dt, Locale locale,
|
||||||
return commonDataTable(dt, locale, "privado", false);
|
Principal principal) {
|
||||||
|
return commonDataTable(dt, locale, "privado", false, principal);
|
||||||
}
|
}
|
||||||
|
|
||||||
private DataTablesResponse<Map<String, Object>> commonDataTable(DataTablesRequest dt, Locale locale, String origen,
|
private DataTablesResponse<Map<String, Object>> commonDataTable(DataTablesRequest dt, Locale locale, String origen,
|
||||||
boolean publico) {
|
boolean publico, Principal principal) {
|
||||||
Long count = repo.findAllByOrigen(Presupuesto.Origen.valueOf(origen)).stream().count();
|
|
||||||
|
Specification<Presupuesto> base = Specification.allOf(
|
||||||
|
(root, query, cb) -> cb.equal(root.get("origen"), Presupuesto.Origen.valueOf(origen)));
|
||||||
|
|
||||||
|
Boolean isAdmin = Utils.isCurrentUserAdmin();
|
||||||
|
if (!isAdmin) {
|
||||||
|
base = base.and((root, query, cb) -> cb.equal(root.get("user").get("id"), Utils.currentUserId(principal)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Long count = repo.count(base);
|
||||||
|
|
||||||
List<String> orderable = List.of(
|
List<String> orderable = List.of(
|
||||||
"id", "titulo", "user.fullName", "tipoEncuadernacion", "tipoCubierta", "tipoImpresion",
|
"id", "titulo", "user.fullName", "tipoEncuadernacion", "tipoCubierta", "tipoImpresion",
|
||||||
@ -74,6 +88,7 @@ public class PresupuestoDatatableService {
|
|||||||
.add("updatedAt", p -> formatDate(p.getUpdatedAt(), locale))
|
.add("updatedAt", p -> formatDate(p.getUpdatedAt(), locale))
|
||||||
.addIf(!publico, "user", p -> p.getUser() != null ? p.getUser().getFullName() : "")
|
.addIf(!publico, "user", p -> p.getUser() != null ? p.getUser().getFullName() : "")
|
||||||
.add("actions", this::generarBotones)
|
.add("actions", this::generarBotones)
|
||||||
|
.where(base)
|
||||||
.toJson(count);
|
.toJson(count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -920,4 +920,28 @@ public class Presupuesto extends AbstractAuditedEntity implements Cloneable {
|
|||||||
public void setId(Long id){
|
public void setId(Long id){
|
||||||
this.id = id;
|
this.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Double getPeso(){
|
||||||
|
// get peso from first element of pricingSnapshotJson (need to parse JSON)
|
||||||
|
// pricingSnapshotJson = {"xxx":{"peso":0.5,...}} is a String
|
||||||
|
if (this.pricingSnapshotJson != null && !this.pricingSnapshotJson.isEmpty()) {
|
||||||
|
try {
|
||||||
|
String json = this.pricingSnapshotJson.trim();
|
||||||
|
int pesoIndex = json.indexOf("\"peso\":");
|
||||||
|
if (pesoIndex != -1) {
|
||||||
|
int startIndex = pesoIndex + 7;
|
||||||
|
int endIndex = json.indexOf(",", startIndex);
|
||||||
|
if (endIndex == -1) {
|
||||||
|
endIndex = json.indexOf("}", startIndex);
|
||||||
|
}
|
||||||
|
String pesoStr = json.substring(startIndex, endIndex).trim();
|
||||||
|
return Double.parseDouble(pesoStr);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// log error
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,83 @@
|
|||||||
|
package com.imprimelibros.erp.redsys;
|
||||||
|
|
||||||
|
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/pagos/redsys")
|
||||||
|
public class RedsysController {
|
||||||
|
|
||||||
|
private final RedsysService service;
|
||||||
|
|
||||||
|
public RedsysController(RedsysService service) {
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/crear")
|
||||||
|
public String crearPago(@RequestParam String order,
|
||||||
|
@RequestParam long amountCents,
|
||||||
|
Model model) throws Exception {
|
||||||
|
|
||||||
|
var req = new RedsysService.PaymentRequest(order, amountCents, "Compra en ImprimeLibros");
|
||||||
|
var form = service.buildRedirectForm(req);
|
||||||
|
model.addAttribute("action", form.action());
|
||||||
|
model.addAttribute("signatureVersion", form.signatureVersion());
|
||||||
|
model.addAttribute("merchantParameters", form.merchantParameters());
|
||||||
|
model.addAttribute("signature", form.signature());
|
||||||
|
return "imprimelibros/payments/redsys-redirect";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/notify")
|
||||||
|
@ResponseBody
|
||||||
|
public ResponseEntity<String> notifyRedsys(
|
||||||
|
@RequestParam("Ds_Signature") String dsSignature,
|
||||||
|
@RequestParam("Ds_MerchantParameters") String dsMerchantParameters) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
RedsysService.RedsysNotification notif = service.validateAndParseNotification(dsSignature,
|
||||||
|
dsMerchantParameters);
|
||||||
|
|
||||||
|
// 1) Idempotencia: comprueba si el pedido ya fue procesado
|
||||||
|
// 2) Valida que importe/moneda/pedido coincidan con lo que esperabas
|
||||||
|
// 3) Marca como pagado si notif.authorized() == true
|
||||||
|
|
||||||
|
return ResponseEntity.ok("OK"); // Redsys espera "OK"
|
||||||
|
} catch (SecurityException se) {
|
||||||
|
// Firma incorrecta: NO procesar
|
||||||
|
return ResponseEntity.status(400).body("BAD SIGNATURE");
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.status(500).body("ERROR");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/ok")
|
||||||
|
public String okReturn(@RequestParam("Ds_Signature") String dsSignature,
|
||||||
|
@RequestParam("Ds_MerchantParameters") String dsMerchantParameters,
|
||||||
|
Model model) {
|
||||||
|
try {
|
||||||
|
RedsysService.RedsysNotification notif = service.validateAndParseNotification(dsSignature, dsMerchantParameters);
|
||||||
|
// Aquí puedes validar importe/pedido/moneda con tu base de datos y marcar como
|
||||||
|
// pagado
|
||||||
|
model.addAttribute("authorized", notif.authorized());
|
||||||
|
//model.addAttribute("order", notif.order());
|
||||||
|
//model.addAttribute("amountCents", notif.amountCents());
|
||||||
|
return "imprimelibros/payments/redsys-ok";
|
||||||
|
} catch (Exception e) {
|
||||||
|
model.addAttribute("error", "No se pudo validar la respuesta de Redsys.");
|
||||||
|
return "imprimelibros/payments/redsys-ko";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/ko")
|
||||||
|
public String koReturn(@RequestParam(value = "Ds_Signature", required = false) String dsSignature,
|
||||||
|
@RequestParam(value = "Ds_MerchantParameters", required = false) String dsMerchantParameters,
|
||||||
|
Model model) {
|
||||||
|
// Suele venir cuando el usuario cancela o hay error
|
||||||
|
model.addAttribute("error", "Operación cancelada o rechazada.");
|
||||||
|
return "imprimelibros/payments/redsys-ko";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
174
src/main/java/com/imprimelibros/erp/redsys/RedsysService.java
Normal file
174
src/main/java/com/imprimelibros/erp/redsys/RedsysService.java
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
package com.imprimelibros.erp.redsys;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import sis.redsys.api.ApiMacSha256;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class RedsysService {
|
||||||
|
|
||||||
|
// ---------- CONFIG ----------
|
||||||
|
@Value("${redsys.merchant-code}")
|
||||||
|
private String merchantCode;
|
||||||
|
@Value("${redsys.terminal}")
|
||||||
|
private String terminal;
|
||||||
|
@Value("${redsys.currency}")
|
||||||
|
private String currency;
|
||||||
|
@Value("${redsys.transaction-type}")
|
||||||
|
private String txType;
|
||||||
|
@Value("${redsys.secret-key}")
|
||||||
|
private String secretKeyBase64;
|
||||||
|
@Value("${redsys.urls.ok}")
|
||||||
|
private String urlOk;
|
||||||
|
@Value("${redsys.urls.ko}")
|
||||||
|
private String urlKo;
|
||||||
|
@Value("${redsys.urls.notify}")
|
||||||
|
private String urlNotify;
|
||||||
|
@Value("${redsys.environment}")
|
||||||
|
private String env;
|
||||||
|
|
||||||
|
// ---------- RECORDS ----------
|
||||||
|
public record PaymentRequest(String order, long amountCents, String description) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record FormPayload(String action, String signatureVersion, String merchantParameters, String signature) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- MÉTODO PRINCIPAL ----------
|
||||||
|
public FormPayload buildRedirectForm(PaymentRequest req) throws Exception {
|
||||||
|
ApiMacSha256 api = new ApiMacSha256();
|
||||||
|
|
||||||
|
api.setParameter("DS_MERCHANT_AMOUNT", String.valueOf(req.amountCents()));
|
||||||
|
api.setParameter("DS_MERCHANT_ORDER", req.order()); // Usa 12 dígitos con ceros si puedes
|
||||||
|
api.setParameter("DS_MERCHANT_MERCHANTCODE", merchantCode);
|
||||||
|
api.setParameter("DS_MERCHANT_CURRENCY", currency);
|
||||||
|
api.setParameter("DS_MERCHANT_TRANSACTIONTYPE", txType);
|
||||||
|
api.setParameter("DS_MERCHANT_TERMINAL", terminal);
|
||||||
|
api.setParameter("DS_MERCHANT_MERCHANTURL", urlNotify);
|
||||||
|
api.setParameter("DS_MERCHANT_URLOK", urlOk);
|
||||||
|
api.setParameter("DS_MERCHANT_URLKO", urlKo);
|
||||||
|
|
||||||
|
String merchantParameters = api.createMerchantParameters();
|
||||||
|
String signature = api.createMerchantSignature(secretKeyBase64);
|
||||||
|
|
||||||
|
String action = "test".equalsIgnoreCase(env)
|
||||||
|
? "https://sis-t.redsys.es:25443/sis/realizarPago"
|
||||||
|
: "https://sis.redsys.es/sis/realizarPago";
|
||||||
|
|
||||||
|
return new FormPayload(action, "HMAC_SHA256_V1", merchantParameters, signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- STEP 3: Decodificar Ds_MerchantParameters ----------
|
||||||
|
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||||
|
|
||||||
|
public Map<String, Object> decodeMerchantParametersToMap(String dsMerchantParametersB64) throws Exception {
|
||||||
|
try {
|
||||||
|
byte[] decoded = Base64.getDecoder().decode(dsMerchantParametersB64);
|
||||||
|
String json = new String(decoded, StandardCharsets.UTF_8);
|
||||||
|
return MAPPER.readValue(json, new TypeReference<>() {
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalArgumentException("No se pudo decodificar Ds_MerchantParameters", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- STEP 4: Validar notificación ----------
|
||||||
|
public RedsysNotification validateAndParseNotification(String dsSignature, String dsMerchantParametersB64)
|
||||||
|
throws Exception {
|
||||||
|
Map<String, Object> mp = decodeMerchantParametersToMap(dsMerchantParametersB64);
|
||||||
|
RedsysNotification notif = new RedsysNotification(mp);
|
||||||
|
|
||||||
|
if (notif.order == null || notif.order.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("Falta Ds_Order en Ds_MerchantParameters");
|
||||||
|
}
|
||||||
|
|
||||||
|
ApiMacSha256 api = new ApiMacSha256();
|
||||||
|
api.setParameter("Ds_MerchantParameters", dsMerchantParametersB64);
|
||||||
|
|
||||||
|
String expected = api.createMerchantSignatureNotif(secretKeyBase64, api.decodeMerchantParameters(dsMerchantParametersB64)); // ✅ SOLO UN PARÁMETRO
|
||||||
|
|
||||||
|
if (!safeEqualsB64(dsSignature, expected)) {
|
||||||
|
throw new SecurityException("Firma Redsys no válida");
|
||||||
|
}
|
||||||
|
|
||||||
|
return notif;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------- HELPERS ----------
|
||||||
|
private static boolean safeEqualsB64(String a, String b) {
|
||||||
|
if (Objects.equals(a, b))
|
||||||
|
return true;
|
||||||
|
try {
|
||||||
|
String na = normalizeB64(a);
|
||||||
|
String nb = normalizeB64(b);
|
||||||
|
byte[] da = Base64.getDecoder().decode(na);
|
||||||
|
byte[] db = Base64.getDecoder().decode(nb);
|
||||||
|
return MessageDigest.isEqual(da, db);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeB64(String s) {
|
||||||
|
if (s == null)
|
||||||
|
return "";
|
||||||
|
String n = s.replace('-', '+').replace('_', '/');
|
||||||
|
int mod = n.length() % 4;
|
||||||
|
if (mod == 2)
|
||||||
|
n += "==";
|
||||||
|
else if (mod == 3)
|
||||||
|
n += "=";
|
||||||
|
else if (mod == 1)
|
||||||
|
n += "===";
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- MODELO DE NOTIFICACIÓN ----------
|
||||||
|
public static final class RedsysNotification {
|
||||||
|
public final Map<String, Object> raw;
|
||||||
|
public final String order;
|
||||||
|
public final String response;
|
||||||
|
public final long amountCents;
|
||||||
|
public final String currency;
|
||||||
|
|
||||||
|
public RedsysNotification(Map<String, Object> raw) {
|
||||||
|
this.raw = raw;
|
||||||
|
this.order = str(raw.get("Ds_Order"));
|
||||||
|
this.response = str(raw.get("Ds_Response"));
|
||||||
|
this.currency = str(raw.get("Ds_Currency"));
|
||||||
|
this.amountCents = parseLongSafe(raw.get("Ds_Amount"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean authorized() {
|
||||||
|
try {
|
||||||
|
int r = Integer.parseInt(response);
|
||||||
|
return r >= 0 && r <= 99;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String str(Object o) {
|
||||||
|
return o == null ? null : String.valueOf(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long parseLongSafe(Object o) {
|
||||||
|
try {
|
||||||
|
return Long.parseLong(String.valueOf(o));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,9 @@
|
|||||||
package com.imprimelibros.erp.users;
|
package com.imprimelibros.erp.users;
|
||||||
|
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
|
|
||||||
|
|||||||
@ -2,10 +2,21 @@ package com.imprimelibros.erp.users;
|
|||||||
|
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
|
|
||||||
|
import java.text.Collator;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.imprimelibros.erp.direcciones.Direccion;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class UserServiceImpl implements UserService {
|
public class UserServiceImpl implements UserService {
|
||||||
|
|
||||||
@ -29,5 +40,4 @@ public class UserServiceImpl implements UserService {
|
|||||||
if (query == null || query.isBlank()) query = null;
|
if (query == null || query.isBlank()) query = null;
|
||||||
return userDao.searchUsers(role, query, pageable);
|
return userDao.searchUsers(role, query, pageable);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
spring.application.name=erp
|
spring.application.name=erp
|
||||||
|
|
||||||
|
server.forward-headers-strategy=framework
|
||||||
|
server.servlet.session.cookie.secure=true
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Logging
|
# Logging
|
||||||
#
|
#
|
||||||
@ -11,8 +15,8 @@ logging.level.org.springframework=ERROR
|
|||||||
#
|
#
|
||||||
# Database Configuration
|
# Database Configuration
|
||||||
#
|
#
|
||||||
spring.datasource.url=jdbc:mysql://localhost:3309/imprimelibros
|
#spring.datasource.url=jdbc:mysql://localhost:3309/imprimelibros
|
||||||
#spring.datasource.url=jdbc:mysql://127.0.0.1:3309/imprimelibros?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Europe/Madrid&characterEncoding=utf8
|
spring.datasource.url=jdbc:mysql://127.0.0.1:3309/imprimelibros?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Europe/Madrid&characterEncoding=utf8
|
||||||
spring.datasource.username=imprimelibros_user
|
spring.datasource.username=imprimelibros_user
|
||||||
spring.datasource.password=om91irrDctd
|
spring.datasource.password=om91irrDctd
|
||||||
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
|
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
|
||||||
@ -101,3 +105,22 @@ spring.liquibase.change-log=classpath:db/changelog/master.yml
|
|||||||
# spring.liquibase.url=jdbc:mysql://localhost:3306/imprimelibros
|
# spring.liquibase.url=jdbc:mysql://localhost:3306/imprimelibros
|
||||||
# spring.liquibase.user=tu_user
|
# spring.liquibase.user=tu_user
|
||||||
# spring.liquibase.password=tu_pass
|
# spring.liquibase.password=tu_pass
|
||||||
|
|
||||||
|
|
||||||
|
# Redsys
|
||||||
|
redsys.environment=test
|
||||||
|
redsys.merchant-code=124760810
|
||||||
|
redsys.terminal=1
|
||||||
|
redsys.currency=978
|
||||||
|
redsys.transaction-type=0
|
||||||
|
redsys.secret-key=sq7HjrUOBfKmC576ILgskD5srU870gJ7
|
||||||
|
redsys.urls.ok=http://localhost:8080/pagos/redsys/ok
|
||||||
|
redsys.urls.ko=http://localhost:8080/pagos/redsys/ko
|
||||||
|
redsys.urls.notify=http://localhost:8080/pagos/redsys/notify
|
||||||
|
|
||||||
|
|
||||||
|
# Mensajes de error mas cortos
|
||||||
|
# Oculta el stack trace en los errores del servidor
|
||||||
|
server.error.include-stacktrace=never
|
||||||
|
# No mostrar el mensaje completo de excepción en la respuesta
|
||||||
|
server.error.include-message=always
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
databaseChangeLog:
|
||||||
|
- changeSet:
|
||||||
|
id: 0005-add-carts-onlyoneshipment
|
||||||
|
author: jjo
|
||||||
|
preConditions:
|
||||||
|
onFail: MARK_RAN
|
||||||
|
not:
|
||||||
|
columnExists:
|
||||||
|
tableName: carts
|
||||||
|
columnName: only_one_shipment
|
||||||
|
|
||||||
|
changes:
|
||||||
|
- addColumn:
|
||||||
|
tableName: carts
|
||||||
|
columns:
|
||||||
|
- column:
|
||||||
|
name: only_one_shipment
|
||||||
|
type: TINYINT(1)
|
||||||
|
defaultValueNumeric: 1
|
||||||
|
remarks: "Si 1, el carrito tiene la misma direccion para todos los items"
|
||||||
|
afterColumn: currency
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
|
||||||
|
- column:
|
||||||
|
name: total
|
||||||
|
type: DECIMAL(19,6)
|
||||||
|
defaultValueNumeric: 0
|
||||||
|
remarks: "Total del carrito"
|
||||||
|
afterColumn: only_one_shipment
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
|
||||||
|
rollback:
|
||||||
|
- dropColumn:
|
||||||
|
tableName: carts
|
||||||
|
columnName: only_one_shipment
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
databaseChangeLog:
|
||||||
|
- changeSet:
|
||||||
|
id: 0006-add-cart-direcciones
|
||||||
|
author: jjo
|
||||||
|
preConditions:
|
||||||
|
onFail: MARK_RAN
|
||||||
|
not:
|
||||||
|
tableExists:
|
||||||
|
tableName: cart_direcciones
|
||||||
|
|
||||||
|
changes:
|
||||||
|
- createTable:
|
||||||
|
tableName: cart_direcciones
|
||||||
|
remarks: "Relación de direcciones y unidades por carrito/direcciones_envio"
|
||||||
|
columns:
|
||||||
|
- column:
|
||||||
|
name: id
|
||||||
|
type: BIGINT UNSIGNED AUTO_INCREMENT
|
||||||
|
constraints:
|
||||||
|
primaryKey: true
|
||||||
|
primaryKeyName: pk_cart_direcciones
|
||||||
|
|
||||||
|
- column:
|
||||||
|
name: cart_id
|
||||||
|
type: BIGINT
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
|
||||||
|
- column:
|
||||||
|
name: direccion_id
|
||||||
|
type: BIGINT
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
|
||||||
|
- column:
|
||||||
|
name: presupuesto_id
|
||||||
|
type: BIGINT
|
||||||
|
constraints:
|
||||||
|
nullable: true
|
||||||
|
|
||||||
|
- column:
|
||||||
|
name: unidades
|
||||||
|
type: INT
|
||||||
|
constraints:
|
||||||
|
nullable: true
|
||||||
|
|
||||||
|
- column:
|
||||||
|
name: is_palets
|
||||||
|
type: TINYINT(1)
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
defaultValue: false
|
||||||
|
|
||||||
|
- createIndex:
|
||||||
|
indexName: idx_cart_dir_direccion_id
|
||||||
|
tableName: cart_direcciones
|
||||||
|
columns:
|
||||||
|
- column:
|
||||||
|
name: direccion_id
|
||||||
|
|
||||||
|
- createIndex:
|
||||||
|
indexName: idx_cart_dir_presupuesto_id
|
||||||
|
tableName: cart_direcciones
|
||||||
|
columns:
|
||||||
|
- column:
|
||||||
|
name: presupuesto_id
|
||||||
|
|
||||||
|
- addForeignKeyConstraint:
|
||||||
|
baseTableName: cart_direcciones
|
||||||
|
baseColumnNames: direccion_id
|
||||||
|
constraintName: fk_cart_dir_direccion
|
||||||
|
referencedTableName: direcciones
|
||||||
|
referencedColumnNames: id
|
||||||
|
onDelete: CASCADE
|
||||||
|
onUpdate: CASCADE
|
||||||
|
|
||||||
|
- addForeignKeyConstraint:
|
||||||
|
baseTableName: cart_direcciones
|
||||||
|
baseColumnNames: presupuesto_id
|
||||||
|
constraintName: fk_cart_dir_presupuesto
|
||||||
|
referencedTableName: presupuesto
|
||||||
|
referencedColumnNames: id
|
||||||
|
onDelete: SET NULL
|
||||||
|
onUpdate: CASCADE
|
||||||
|
|
||||||
|
rollback:
|
||||||
|
- dropForeignKeyConstraint:
|
||||||
|
baseTableName: cart_direcciones
|
||||||
|
constraintName: fk_cart_dir_direccion
|
||||||
|
- dropForeignKeyConstraint:
|
||||||
|
baseTableName: cart_direcciones
|
||||||
|
constraintName: fk_cart_dir_presupuesto
|
||||||
|
- dropTable:
|
||||||
|
tableName: cart_direcciones
|
||||||
@ -6,4 +6,8 @@ databaseChangeLog:
|
|||||||
- include:
|
- include:
|
||||||
file: db/changelog/changesets/0003-create-paises.yml
|
file: db/changelog/changesets/0003-create-paises.yml
|
||||||
- include:
|
- include:
|
||||||
file: db/changelog/changesets/0004-create-direcciones.yml
|
file: db/changelog/changesets/0004-create-direcciones.yml
|
||||||
|
- include:
|
||||||
|
file: db/changelog/changesets/0005-add-carts-onlyoneshipment.yml
|
||||||
|
- include:
|
||||||
|
file: db/changelog/changesets/0006-add-cart-direcciones.yml
|
||||||
@ -3,6 +3,7 @@ app.yes=Sí
|
|||||||
app.no=No
|
app.no=No
|
||||||
app.aceptar=Aceptar
|
app.aceptar=Aceptar
|
||||||
app.cancelar=Cancelar
|
app.cancelar=Cancelar
|
||||||
|
app.seleccionar=Seleccionar
|
||||||
app.guardar=Guardar
|
app.guardar=Guardar
|
||||||
app.editar=Editar
|
app.editar=Editar
|
||||||
app.add=Añadir
|
app.add=Añadir
|
||||||
|
|||||||
@ -4,11 +4,47 @@ cart.empty=Tu cesta de la compra está vacía.
|
|||||||
cart.item.presupuesto-numero=Presupuesto #
|
cart.item.presupuesto-numero=Presupuesto #
|
||||||
cart.precio=Precio
|
cart.precio=Precio
|
||||||
|
|
||||||
|
cart.tabs.details=Detalles
|
||||||
|
cart.tabs.envio=Envío
|
||||||
|
|
||||||
|
cart.shipping.add=Añadir dirección
|
||||||
|
cart.shipping.add.title=Seleccione una dirección
|
||||||
|
cart.shipping.select-placeholder=Buscar en direcciones...
|
||||||
|
cart.shipping.new-address=Nueva dirección
|
||||||
|
cart.shipping.info=Todos los pedidos incluyen un envío gratuito a la Península y Baleares por línea de pedido.
|
||||||
|
cart.shipping.order=Envío del pedido
|
||||||
|
cart.shipping.samples=Envío de prueba
|
||||||
|
cart.shipping.onlyOneShipment=Todo el pedido se envía a una única dirección.
|
||||||
|
cart.shipping.tirada=Tirada:
|
||||||
|
cart.shipping.unidades=unidades
|
||||||
|
cart.shipping.ud=ud.
|
||||||
|
cart.shipping.uds=uds.
|
||||||
|
cart.shipping.enter-units=Introduzca el número de unidades para esta dirección:
|
||||||
|
cart.shipping.units-label=Número de unidades (máximo {max})
|
||||||
|
cart.shipping.send-in-palets=Enviar en palets
|
||||||
|
cart.shipping.send-in-palets.info=Marque esta opción si desea que el envío se realice en palets (sólo para tiradas grandes). La entrega se realizará a pie de calle.
|
||||||
|
cart.shipping.tipo-envio=Tipo de envío:
|
||||||
|
|
||||||
|
cart.shipping.errors.units-error=Por favor, introduzca un número válido entre 1 y {max}.
|
||||||
|
cart.shipping.errors.noAddressSelected=Debe seleccionar una dirección de envío para el pedido.
|
||||||
|
cart.shipping.errors.fillAddressesItems=Debe seleccionar una dirección de envío para cada artículo de la cesta.
|
||||||
|
|
||||||
cart.resumen.title=Resumen de la cesta
|
cart.resumen.title=Resumen de la cesta
|
||||||
cart.resumen.base=Base imponible:
|
cart.resumen.base=Base imponible
|
||||||
cart.resumen.iva-4=IVA 4%:
|
cart.resumen.envio=Coste de envío
|
||||||
cart.resumen.iva-21=IVA 21%:
|
cart.resumen.iva-4=IVA 4%
|
||||||
cart.resumen.total=Total cesta:
|
cart.resumen.iva-21=IVA 21%
|
||||||
|
cart.resumen.descuento=Descuento fidelización
|
||||||
|
cart.resumen.total=Total cesta
|
||||||
cart.resumen.tramitar=Tramitar pedido
|
cart.resumen.tramitar=Tramitar pedido
|
||||||
|
|
||||||
cart.resumen.fidelizacion=Si tiene descuento por fidelización, se aplicará al tramitar el pedido.
|
cart.pass-to.customer=Mover cesta a cliente
|
||||||
|
cart.pass-to.customer.info=Puede mover la cesta actual al cliente seleccionado. Esto eliminará la cesta del usuario actual y la asociará al cliente seleccionado.
|
||||||
|
cart.pass-to.customer.warning=Advertencia: Esta acción no se puede deshacer y sobrescribirá la cesta del cliente seleccionado. Asegúrese de que el cliente seleccionado es correcto.
|
||||||
|
cart.pass-to.select-customer=Seleccione un cliente
|
||||||
|
cart.pass-to.button=Mover cesta
|
||||||
|
cart.pass-to.success=Cesta movida correctamente al cliente {0}.
|
||||||
|
cart.pass-to.customer.error=Debe seleccionar un cliente para mover la cesta.
|
||||||
|
cart.pass-to.customer.error-move=Error al mover la cesta de la compra
|
||||||
|
cart.errors.update-cart=Error al actualizar la cesta de la compra: {0}
|
||||||
|
cart.errors.shipping=No se puede calcular el coste del envío para alguna de las direcciones seleccionadas. Por favor, póngase en contacto con el servicio de atención al cliente.
|
||||||
@ -53,5 +53,7 @@ direcciones.error.delete-internal-error=Error interno al eliminar la dirección.
|
|||||||
direcciones.error.noEncontrado=Dirección no encontrada.
|
direcciones.error.noEncontrado=Dirección no encontrada.
|
||||||
direcciones.error.sinPermiso=No tiene permiso para realizar esta acción.
|
direcciones.error.sinPermiso=No tiene permiso para realizar esta acción.
|
||||||
|
|
||||||
|
direcciones.error.noShippingCost=No se pudo calcular el coste de envío para la dirección proporcionada.
|
||||||
|
|
||||||
direcciones.form.error.required=Campo obligatorio.
|
direcciones.form.error.required=Campo obligatorio.
|
||||||
|
|
||||||
|
|||||||
0
src/main/resources/i18n/pedidos_en.properties
Normal file
0
src/main/resources/i18n/pedidos_en.properties
Normal file
17
src/main/resources/i18n/pedidos_es.properties
Normal file
17
src/main/resources/i18n/pedidos_es.properties
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
checkout.title=Finalizar compra
|
||||||
|
checkout.summay=Resumen de la compra
|
||||||
|
checkout.shipping=Envío
|
||||||
|
checkout.payment=Método de pago
|
||||||
|
|
||||||
|
checkout.shipping.info=Todos los pedidos incluyen un envío gratuito a la Península y Baleares por línea de pedido.
|
||||||
|
checkout.shipping.order=Envío del pedido
|
||||||
|
checkout.shipping.samples=Envío de pruebas
|
||||||
|
checkout.shipping.onlyOneShipment=Todo el pedido se envía a una única dirección.
|
||||||
|
|
||||||
|
|
||||||
|
checkout.summary.presupuesto=#Presupuesto
|
||||||
|
checkout.summary.titulo=Título
|
||||||
|
checkout.summary.base=Base
|
||||||
|
checkout.summary.iva-4=IVA 4%
|
||||||
|
checkout.summary.iva-21=IVA 21%
|
||||||
|
checkout.summary.envio=Envío
|
||||||
BIN
src/main/resources/lib/apiSha256.jar
Normal file
BIN
src/main/resources/lib/apiSha256.jar
Normal file
Binary file not shown.
BIN
src/main/resources/lib/bcprov-jdk15on-1.4.7.jar
Normal file
BIN
src/main/resources/lib/bcprov-jdk15on-1.4.7.jar
Normal file
Binary file not shown.
BIN
src/main/resources/lib/commons-codec-1.3.jar
Normal file
BIN
src/main/resources/lib/commons-codec-1.3.jar
Normal file
Binary file not shown.
BIN
src/main/resources/lib/org.json.jar
Normal file
BIN
src/main/resources/lib/org.json.jar
Normal file
Binary file not shown.
38
src/main/resources/static/assets/css/cart.css
Normal file
38
src/main/resources/static/assets/css/cart.css
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
.nav-pills .nav-link.active, .nav-pills .show > .nav-link {
|
||||||
|
background-color: #4c5c6366 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-arrow-nav .nav {
|
||||||
|
background-color: #4c5c6333 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link.active .bg-soft-primary {
|
||||||
|
background-color: #ffffff33 !important;
|
||||||
|
/* #4c5c63 al 20% */
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link.active .text-primary {
|
||||||
|
color: #ffffff !important;
|
||||||
|
/* #4c5c63 al 20% */
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:not(.active) .bg-soft-primary {
|
||||||
|
background-color: #4c5c6366 !important;
|
||||||
|
/* #4c5c63 al 20% */
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:not(.active) .text-primary {
|
||||||
|
color: #000000 !important;
|
||||||
|
/* #4c5c63 al 20% */
|
||||||
|
}
|
||||||
|
|
||||||
|
.direccion-card {
|
||||||
|
flex: 1 1 350px; /* ancho mínimo 350px, crece si hay espacio */
|
||||||
|
max-width: 350px; /* opcional, para que no se estiren demasiado */
|
||||||
|
min-width: 340px; /* protege el ancho mínimo */
|
||||||
|
}
|
||||||
|
|
||||||
|
.shipping-addresses-item { align-items: stretch; justify-content: center;}
|
||||||
|
.shipping-addresses-sample { align-items: stretch; justify-content: center;}
|
||||||
|
.direccion-card { display: flex; flex-direction: column; }
|
||||||
|
.direccion-card .card-body { display: flex; flex-direction: column; }
|
||||||
@ -29,3 +29,13 @@ body {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
border-color: #92b2a7;
|
border-color: #92b2a7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Solo dentro del modal */
|
||||||
|
.swal2-popup .form-switch-custom {
|
||||||
|
font-size: 1rem; /* clave: fija el tamaño base del switch */
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.swal2-popup .form-switch-custom .form-check-input {
|
||||||
|
float: none; /* por si acaso */
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,44 +1,111 @@
|
|||||||
import { formateaMoneda } from '../../imprimelibros/utils.js';
|
import { formateaMoneda } from '../../imprimelibros/utils.js';
|
||||||
|
import { showLoader, hideLoader } from '../loader.js';
|
||||||
|
|
||||||
$(() => {
|
$(() => {
|
||||||
|
|
||||||
updateTotal();
|
$(document).ajaxStart(showLoader).ajaxStop(hideLoader);
|
||||||
|
|
||||||
function updateTotal() {
|
$(document).on('updateCart', () => {
|
||||||
const items = $(".product");
|
// get form and submit
|
||||||
let iva4 = 0;
|
const form = $('#cartForm');
|
||||||
let iva21 = 0;
|
const container = $("#onlyOneShipment").is(':checked') ? $('#shippingAddressesContainer') : $('.product');
|
||||||
let base = 0;
|
// remove name from container . direccion-card
|
||||||
for (let i = 0; i < items.length; i++) {
|
container.find('.direccion-card input[type="hidden"]').removeAttr('name');
|
||||||
const item = $(items[i]);
|
|
||||||
const b = item.data("base");
|
container.find('.direccion-card').each(function (i) {
|
||||||
const i4 = item.data("iva-4");
|
$(this).find('.direccion-id').attr('name', 'direcciones[' + i + '].id');
|
||||||
const i21 = item.data("iva-21");
|
$(this).find('.direccion-cp').attr('name', 'direcciones[' + i + '].cp');
|
||||||
base += parseFloat(b) || 0;
|
$(this).find('.direccion-pais-code3').attr('name', 'direcciones[' + i + '].paisCode3');
|
||||||
iva4 += parseFloat(i4) || 0;
|
if ($(this).find('.presupuesto-id').length > 0 && $(this).find('.presupuesto-id').val() !== null
|
||||||
iva21 += parseFloat(i21) || 0;
|
&& $(this).find('.presupuesto-id').val() !== "")
|
||||||
|
$(this).find('.presupuesto-id').attr('name', 'direcciones[' + i + '].presupuestoId');
|
||||||
|
if ($(this).find('.item-tirada').length > 0 && $(this).find('.item-tirada').val() !== null
|
||||||
|
&& $(this).find('.item-tirada').val() !== "")
|
||||||
|
$(this).find('.item-tirada').attr('name', 'direcciones[' + i + '].unidades');
|
||||||
|
});
|
||||||
|
$.post(form.attr('action'), form.serialize(), (response) => {
|
||||||
|
// if success and received html, replace container summary
|
||||||
|
if (response) {
|
||||||
|
$('.cart-summary-container').replaceWith(response);
|
||||||
|
}
|
||||||
|
}).always(() => {
|
||||||
|
hideLoader();
|
||||||
|
});
|
||||||
|
|
||||||
|
checkAddressesForItems();
|
||||||
|
});
|
||||||
|
|
||||||
|
checkAddressesForItems();
|
||||||
|
|
||||||
|
function checkAddressesForItems() {
|
||||||
|
if ($('.product').length === 0) {
|
||||||
|
$("#alert-empty").removeClass("d-none");
|
||||||
|
$('.cart-content').addClass('d-none');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
$("#base-cesta").text(formateaMoneda(base));
|
else {
|
||||||
if (iva4 > 0) {
|
$('.cart-content').removeClass('d-none');
|
||||||
$("#iva-4-cesta").text(formateaMoneda(iva4));
|
$("#alert-empty").addClass("d-none");
|
||||||
$("#tr-iva-4").show();
|
// check if select2 is initialized
|
||||||
} else {
|
if ($('#select-customer').length && !$('#select-customer').hasClass('select2-hidden-accessible')) {
|
||||||
$("#tr-iva-4").hide();
|
initMoveCartToCustomer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (iva21 > 0) {
|
if ($('#onlyOneShipment').is(':checked')) {
|
||||||
$("#iva-21-cesta").text(formateaMoneda(iva21));
|
if ($("#shippingAddressesContainer .direccion-card").length === 0) {
|
||||||
$("#tr-iva-21").show();
|
$(".alert-shipment").removeClass("d-none");
|
||||||
} else {
|
$('#btn-checkout').prop('disabled', true);
|
||||||
$("#tr-iva-21").hide();
|
return;
|
||||||
|
}
|
||||||
|
$(".alert-shipment").addClass("d-none");
|
||||||
|
$('#btn-checkout').prop('disabled', false);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const items = $(".product");
|
||||||
|
let errorFound = false;
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
let errorFoundItem = false;
|
||||||
|
const item = $(items[i]);
|
||||||
|
const tirada = parseInt(item.find(".item-tirada").val()) || 0;
|
||||||
|
const direcciones = item.find(".direccion-card");
|
||||||
|
let totalUnidades = 0;
|
||||||
|
direcciones.each(function () {
|
||||||
|
const unidades = parseInt($(this).find(".item-tirada").val()) || 0;
|
||||||
|
totalUnidades += unidades;
|
||||||
|
});
|
||||||
|
if (totalUnidades < tirada) {
|
||||||
|
errorFoundItem = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.find(".shipping-addresses-sample")) {
|
||||||
|
const container = item.find(".shipping-addresses-sample");
|
||||||
|
if (container.find('.direccion-card').toArray().length === 0) {
|
||||||
|
errorFoundItem = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (errorFoundItem) {
|
||||||
|
errorFound = true;
|
||||||
|
item.find(".alert-icon-shipment").removeClass("d-none");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
item.find(".alert-icon-shipment").addClass("d-none");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (errorFound) {
|
||||||
|
$(".alert-shipment").removeClass("d-none");
|
||||||
|
$('#btn-checkout').prop('disabled', true);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$(".alert-shipment").addClass("d-none");
|
||||||
|
$('#btn-checkout').prop('disabled', false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const total = base + iva4 + iva21;
|
|
||||||
$("#total-cesta").text(formateaMoneda(total));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).on("click", ".delete-item", async function (event) {
|
$(document).on("click", ".delete-item", async function (event) {
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const cartItemId = $(this).data("cart-item-id");
|
const presupuestoId = $(this).data("cart-item-id");
|
||||||
const card = $(this).closest('.card.product');
|
const card = $(this).closest('.card.product');
|
||||||
|
|
||||||
// CSRF (Spring Security)
|
// CSRF (Spring Security)
|
||||||
@ -46,7 +113,7 @@ $(() => {
|
|||||||
const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.content || 'X-CSRF-TOKEN';
|
const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.content || 'X-CSRF-TOKEN';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/cart/delete/item/${cartItemId}`, {
|
const res = await fetch(`/cart/delete/item/${presupuestoId}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: { [csrfHeader]: csrfToken }
|
headers: { [csrfHeader]: csrfToken }
|
||||||
});
|
});
|
||||||
@ -55,13 +122,81 @@ $(() => {
|
|||||||
console.error('Error al eliminar. Status:', res.status);
|
console.error('Error al eliminar. Status:', res.status);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else{
|
else {
|
||||||
card?.remove();
|
card?.remove();
|
||||||
updateTotal();
|
$(document).trigger('updateCart');
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error en la solicitud:', err);
|
console.error('Error en la solicitud:', err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function initMoveCartToCustomer() {
|
||||||
|
if ($('#select-customer').length) {
|
||||||
|
|
||||||
|
$('#moveCart').on('click', async function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const customerId = $('#select-customer').val();
|
||||||
|
if (!customerId) {
|
||||||
|
// set text and show alert
|
||||||
|
$('#alert-select-customer').text(window.languageBundle['cart.pass-to.customer.error'] || 'Debe seleccionar un cliente para mover la cesta.');
|
||||||
|
$('#alert-select-customer').removeClass('d-none').hide().fadeIn();
|
||||||
|
setTimeout(() => {
|
||||||
|
$('#alert-select-customer').fadeOut(function () {
|
||||||
|
$(this).addClass('d-none');
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSRF (Spring Security)
|
||||||
|
const csrfToken = document.querySelector('meta[name="_csrf"]')?.content || '';
|
||||||
|
const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.content || 'X-CSRF-TOKEN';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/cart/pass-to-customer/${customerId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { [csrfHeader]: csrfToken }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
$('#alert-select-customer').text(window.languageBundle['cart.pass-to.customer.move.error'] || 'Error al mover la cesta de la compra');
|
||||||
|
$('#alert-select-customer').removeClass('d-none').hide().fadeIn();
|
||||||
|
setTimeout(() => {
|
||||||
|
$('#alert-select-customer').fadeOut(function () {
|
||||||
|
$(this).addClass('d-none');
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
window.location.href = '/cart';
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error en la solicitud:', err);
|
||||||
|
$('#alert-select-customer').text(window.languageBundle['cart.errors.move-cart'] || 'Error al mover la cesta de la compra');
|
||||||
|
$('#alert-select-customer').removeClass('d-none').hide().fadeIn();
|
||||||
|
setTimeout(() => {
|
||||||
|
$('#alert-select-customer').fadeOut(function () {
|
||||||
|
$(this).addClass('d-none');
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#select-customer').select2({
|
||||||
|
width: '100%',
|
||||||
|
ajax: {
|
||||||
|
url: 'users/api/get-users',
|
||||||
|
dataType: 'json',
|
||||||
|
delay: 250,
|
||||||
|
},
|
||||||
|
allowClear: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
initMoveCartToCustomer();
|
||||||
|
|
||||||
});
|
});
|
||||||
@ -0,0 +1,443 @@
|
|||||||
|
import { showLoader, hideLoader } from '../loader.js';
|
||||||
|
|
||||||
|
$(() => {
|
||||||
|
|
||||||
|
// Si usas jQuery AJAX:
|
||||||
|
$(document).ajaxStart(showLoader).ajaxStop(hideLoader);
|
||||||
|
|
||||||
|
$("#onlyOneShipment").on('change', function () {
|
||||||
|
if ($(this).is(':checked')) {
|
||||||
|
$('.nav-product').addClass('d-none');
|
||||||
|
document.querySelectorAll('.card.product').forEach(card => {
|
||||||
|
const detailsBtn = card.querySelector('.nav-link[id^="pills-details-"][id$="-tab"]');
|
||||||
|
if (detailsBtn) new bootstrap.Tab(detailsBtn).show();
|
||||||
|
});
|
||||||
|
$('#shippingAddressesContainer').empty().removeClass('d-none');
|
||||||
|
$('.shipping-addresses-item').toArray().forEach(element => {
|
||||||
|
$(element).empty().addClass('d-none');
|
||||||
|
});
|
||||||
|
$('#addOrderAddress').removeClass('d-none');
|
||||||
|
} else {
|
||||||
|
$('.nav-product').removeClass('d-none');
|
||||||
|
$('#shippingAddressesContainer').empty().addClass('d-none');
|
||||||
|
$('.shipping-addresses-item').toArray().forEach(element => {
|
||||||
|
$(element).empty().removeClass('d-none');
|
||||||
|
});
|
||||||
|
$('#addOrderAddress').addClass('d-none');
|
||||||
|
}
|
||||||
|
$(document).trigger('updateCart');
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.btn-delete-direccion', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const $card = $(this).closest('.direccion-card');
|
||||||
|
const $div = $card.parent();
|
||||||
|
$card.remove();
|
||||||
|
if ($div.hasClass('shipping-order-address')) {
|
||||||
|
$('#addOrderAddress').removeClass('d-none');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$div.trigger('direcciones:actualizadas');
|
||||||
|
}
|
||||||
|
$(document).trigger('updateCart');
|
||||||
|
});
|
||||||
|
|
||||||
|
//btn-edit-direccion
|
||||||
|
$(document).on('click', '.btn-edit-direccion', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const $card = $(this).closest('.direccion-card');
|
||||||
|
const container = $(this).closest('.product').find('.shipping-addresses-item');
|
||||||
|
const tirada = $(this).closest('.product').find('.item-tirada').val();
|
||||||
|
const totalTirada = container.find('.item-tirada').toArray().reduce((acc, el) => acc + parseInt($(el).val() || 0), 0);
|
||||||
|
const remainingTirada = parseInt(tirada) - parseInt(totalTirada) + parseInt($card.find('.item-tirada').val() || 0);
|
||||||
|
const data = getUnitsFromUser(remainingTirada);
|
||||||
|
data.then(data => {
|
||||||
|
if (data.unidades) {
|
||||||
|
$card.find('.item-tirada').val(data.unidades);
|
||||||
|
$card.find('#units-text').each(function () {
|
||||||
|
if (data.unidades == 1) {
|
||||||
|
$(this).text(`${data.unidades} ${window.languageBundle['cart.shipping.ud'] || 'unidad'}`);
|
||||||
|
} else {
|
||||||
|
$(this).text(`${data.unidades} ${window.languageBundle['cart.shipping.uds'] || 'unidades'}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
container.trigger('direcciones:actualizadas');
|
||||||
|
$(document).trigger('updateCart');
|
||||||
|
$card.find('.is-palets').val(data.isPalets ? 'true' : 'false');
|
||||||
|
$card.find('.icon-shipment').each(function () {
|
||||||
|
if (data.isPalets) {
|
||||||
|
$(this).removeClass('la-box').addClass('la-pallet');
|
||||||
|
} else {
|
||||||
|
$(this).removeClass('la-pallet').addClass('la-box');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
const language = document.documentElement.lang || 'es-ES';
|
||||||
|
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('');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#addOrderAddress').on('click', async () => {
|
||||||
|
if ($('#onlyOneShipment').is(':checked')) {
|
||||||
|
if (await seleccionarDireccionEnvio()) {
|
||||||
|
$('#addOrderAddress').addClass('d-none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.btn-add-shipping', function () {
|
||||||
|
const presupuestoId = $(this).closest('.product').find('.item-presupuesto-id').val();
|
||||||
|
const container = $(this).closest('.product').find('.shipping-addresses-item');
|
||||||
|
const tirada = $(this).closest('.product').find('.item-tirada').val();
|
||||||
|
const totalTirada = container.find('.item-tirada').toArray().reduce((acc, el) => acc + parseInt($(el).val() || 0), 0);
|
||||||
|
const remainingTirada = parseInt(tirada) - parseInt(totalTirada);
|
||||||
|
seleccionarDireccionEnvio(presupuestoId, remainingTirada, container);
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.btn-add-shipping-sample', function () {
|
||||||
|
const presupuestoId = $(this).closest('.product').find('.item-presupuesto-id').val();
|
||||||
|
const container = $(this).closest('.product').find('.shipping-addresses-sample');
|
||||||
|
seleccionarDireccionEnvio(presupuestoId, null, container);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function seleccionarDireccionEnvio(presupuestoId = null, tirada = null, container = null) {
|
||||||
|
|
||||||
|
const { value: direccionId, isDenied } = await Swal.fire({
|
||||||
|
title: window.languageBundle['cart.shipping.add.title'] || 'Seleccione una dirección',
|
||||||
|
html: `
|
||||||
|
<select id="direccionSelect" class="form-select" style="width: 100%"></select>
|
||||||
|
`,
|
||||||
|
showCancelButton: true,
|
||||||
|
showDenyButton: true,
|
||||||
|
buttonsStyling: false,
|
||||||
|
confirmButtonText: window.languageBundle['app.seleccionar'] || 'Seleccionar',
|
||||||
|
cancelButtonText: window.languageBundle['app.cancelar'] || 'Cancelar',
|
||||||
|
denyButtonText: window.languageBundle['cart.shipping.new-address'] || 'Nueva dirección',
|
||||||
|
customClass: {
|
||||||
|
confirmButton: 'btn btn-secondary me-2',
|
||||||
|
cancelButton: 'btn btn-light',
|
||||||
|
denyButton: 'btn btn-secondary me-2'
|
||||||
|
},
|
||||||
|
focusConfirm: false,
|
||||||
|
|
||||||
|
// Inicializa cuando el DOM del modal ya existe
|
||||||
|
didOpen: () => {
|
||||||
|
const $select = $('#direccionSelect');
|
||||||
|
$select.empty(); // limpia placeholder estático
|
||||||
|
|
||||||
|
$select.select2({
|
||||||
|
width: '100%',
|
||||||
|
dropdownParent: $('.swal2-container'),
|
||||||
|
ajax: {
|
||||||
|
url: '/direcciones/select2',
|
||||||
|
dataType: 'json',
|
||||||
|
delay: 250,
|
||||||
|
data: params => ({ q: params.term || '' }),
|
||||||
|
processResults: (data) => {
|
||||||
|
const items = Array.isArray(data) ? data : (data.results || []);
|
||||||
|
return {
|
||||||
|
results: items.map(item => ({
|
||||||
|
id: item.id,
|
||||||
|
text: item.text, // ← Select2 necesita 'id' y 'text'
|
||||||
|
alias: item.alias || 'Sin alias',
|
||||||
|
att: item.att || '',
|
||||||
|
direccion: item.direccion || '',
|
||||||
|
cp: item.cp || '',
|
||||||
|
ciudad: item.ciudad || '',
|
||||||
|
html: `
|
||||||
|
<div>
|
||||||
|
<strong>${item.alias || 'Sin alias'}</strong><br>
|
||||||
|
${item.att ? `<small>${item.att}</small><br>` : ''}
|
||||||
|
<small>${item.direccion || ''}${item.cp ? ', ' + item.cp : ''}${item.ciudad ? ', ' + item.ciudad : ''}</small>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})),
|
||||||
|
pagination: { more: false } // opcional, evita que espere más páginas
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
placeholder: window.languageBundle['cart.shipping.select-placeholder'] || 'Buscar en direcciones...',
|
||||||
|
language: language,
|
||||||
|
|
||||||
|
templateResult: data => {
|
||||||
|
if (data.loading) return data.text;
|
||||||
|
return $(data.html || data.text);
|
||||||
|
},
|
||||||
|
// Selección más compacta (solo alias + ciudad)
|
||||||
|
templateSelection: data => {
|
||||||
|
if (!data.id) return data.text;
|
||||||
|
const alias = data.alias || data.text;
|
||||||
|
const ciudad = data.ciudad ? ` — ${data.ciudad}` : '';
|
||||||
|
return $(`<span>${alias}${ciudad}</span>`);
|
||||||
|
},
|
||||||
|
escapeMarkup: m => m
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
preConfirm: () => {
|
||||||
|
const $select = $('#direccionSelect');
|
||||||
|
const val = $select.val();
|
||||||
|
if (!val) {
|
||||||
|
Swal.showValidationMessage(
|
||||||
|
window.languageBundle['cart.shipping.errors.noAddressSelected'] || 'Por favor, seleccione una dirección.'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
|
||||||
|
didClose: () => {
|
||||||
|
// Limpieza: destruir select2 para evitar fugas
|
||||||
|
const $select = $('#direccionSelect');
|
||||||
|
if ($select.data('select2')) {
|
||||||
|
$select.select2('destroy');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isDenied) {
|
||||||
|
$.get('/direcciones/direction-form', function (html) {
|
||||||
|
$('#direccionFormModalBody').html(html);
|
||||||
|
const title = $('#direccionFormModalBody #direccionForm').data('add');
|
||||||
|
$('#direccionFormModal .modal-title').text(title);
|
||||||
|
modal.removeClass('d-none');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let unidades = null;
|
||||||
|
let isPalets = 0;
|
||||||
|
if (tirada !== null && tirada >= 1 && direccionId) {
|
||||||
|
|
||||||
|
const data = await getUnitsFromUser(tirada);
|
||||||
|
if (data && data.unidades) {
|
||||||
|
unidades = parseInt(data.unidades);
|
||||||
|
isPalets = data.isPalets ? 1 : 0;
|
||||||
|
} else {
|
||||||
|
// Si el usuario cancela, salir de la función
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (presupuestoId == null && direccionId) { // caso para todas los envios a la misma direccion
|
||||||
|
const isPaletsValue = await getTipoEnvio();
|
||||||
|
if (isPaletsValue !== null) {
|
||||||
|
isPalets = isPaletsValue ? 1 : 0;
|
||||||
|
} else {
|
||||||
|
// Si el usuario cancela, salir de la función
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (direccionId) {
|
||||||
|
// Obtén el objeto completo seleccionado
|
||||||
|
showLoader();
|
||||||
|
let uri = `/cart/get-address/${direccionId}?isPalets=${isPalets}`;
|
||||||
|
if (presupuestoId !== null) {
|
||||||
|
uri += `&presupuestoId=${presupuestoId}`;
|
||||||
|
if (tirada !== null) {
|
||||||
|
uri += `&unidades=${unidades}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const response = await fetch(uri);
|
||||||
|
if (response.ok) {
|
||||||
|
const html = await response.text();
|
||||||
|
if (presupuestoId !== null) {
|
||||||
|
container.append(html).trigger('direcciones:actualizadas');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$('#shippingAddressesContainer').append(html);
|
||||||
|
}
|
||||||
|
$(document).trigger('updateCart');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
hideLoader();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
hideLoader();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getUnitsFromUser(tirada) {
|
||||||
|
const { value: formValues } = await Swal.fire({
|
||||||
|
title: window.languageBundle['cart.shipping.enter-units'] || 'Introduzca el número de unidades para esta dirección',
|
||||||
|
html: `
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">
|
||||||
|
${window.languageBundle['cart.shipping.units-label']?.replace('{max}', tirada) || `Número de unidades (máximo ${tirada})`}
|
||||||
|
</label>
|
||||||
|
<input id="swal-input-unidades" type="number" min="1" max="${tirada}" step="1"
|
||||||
|
value="${tirada}" class="form-control text-center">
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-switch form-switch-custom mb-3 d-flex align-items-center justify-content-center ps-0">
|
||||||
|
<input type="checkbox"
|
||||||
|
id="swal-input-palets"
|
||||||
|
class="form-check-input ms-0 me-2 float-none">
|
||||||
|
<label for="swal-input-palets" class="form-check-label mb-0">
|
||||||
|
${window.languageBundle['cart.shipping.send-in-palets'] || 'Enviar en palets'}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<span class="form-text text-muted">
|
||||||
|
${window.languageBundle['cart.shipping.send-in-palets.info'] || 'En palets la entrega se realizará a pie de calle.'}
|
||||||
|
</span>
|
||||||
|
`,
|
||||||
|
focusConfirm: false,
|
||||||
|
showCancelButton: true,
|
||||||
|
buttonsStyling: false,
|
||||||
|
customClass: {
|
||||||
|
confirmButton: 'btn btn-secondary me-2',
|
||||||
|
cancelButton: 'btn btn-light',
|
||||||
|
},
|
||||||
|
confirmButtonText: window.languageBundle['app.aceptar'] || 'Aceptar',
|
||||||
|
cancelButtonText: window.languageBundle['app.cancelar'] || 'Cancelar',
|
||||||
|
preConfirm: () => {
|
||||||
|
const unidades = parseInt(document.getElementById('swal-input-unidades').value, 10);
|
||||||
|
const isPalets = document.getElementById('swal-input-palets').checked;
|
||||||
|
|
||||||
|
if (!unidades || isNaN(unidades) || unidades < 1 || unidades > tirada) {
|
||||||
|
Swal.showValidationMessage(
|
||||||
|
window.languageBundle['cart.shipping.errors.units-error']?.replace('{max}', tirada)
|
||||||
|
|| `Por favor, introduzca un número válido entre 1 y ${tirada}.`
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return { unidades, isPalets };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (formValues) {
|
||||||
|
return formValues; // { unidades: number, isPalets: boolean }
|
||||||
|
}
|
||||||
|
return null; // Si se cancela el Swal
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTipoEnvio() {
|
||||||
|
const { isConfirmed, value } = await Swal.fire({
|
||||||
|
title: window.languageBundle['cart.shipping.tipo-envio'] || 'Tipo de envío',
|
||||||
|
html: `
|
||||||
|
<div class="form-check form-switch form-switch-custom my-3 d-flex align-items-center justify-content-center gap-2">
|
||||||
|
<input type="checkbox" class="form-check-input" id="swal-input-palets">
|
||||||
|
<label for="swal-input-palets" class="form-label mb-0">
|
||||||
|
${window.languageBundle['cart.shipping.send-in-palets'] || 'Enviar en palets'}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<span class="form-text text-muted">
|
||||||
|
${window.languageBundle['cart.shipping.send-in-palets.info'] || 'En palets la entrega se realizará a pie de calle.'}
|
||||||
|
</span>
|
||||||
|
`,
|
||||||
|
focusConfirm: false,
|
||||||
|
showCancelButton: true,
|
||||||
|
buttonsStyling: false,
|
||||||
|
customClass: {
|
||||||
|
confirmButton: 'btn btn-secondary me-2',
|
||||||
|
cancelButton: 'btn btn-light',
|
||||||
|
},
|
||||||
|
confirmButtonText: window.languageBundle['app.aceptar'] || 'Aceptar',
|
||||||
|
cancelButtonText: window.languageBundle['app.cancelar'] || 'Cancelar',
|
||||||
|
preConfirm: () => {
|
||||||
|
const popup = Swal.getPopup();
|
||||||
|
const chk = popup.querySelector('#swal-input-palets');
|
||||||
|
// Devuelve un OBJETO (siempre truthy) con el booleano dentro
|
||||||
|
return { isPalets: !!chk?.checked };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isConfirmed) return null; // cancelado
|
||||||
|
return value.isPalets; // true / false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function checkTotalUnits(container, tirada) {
|
||||||
|
|
||||||
|
const totalUnits = container.find('.direccion-card').toArray().reduce((acc, el) => {
|
||||||
|
const unidades = parseInt($(el).find('.item-tirada').val()) || 0;
|
||||||
|
return acc + unidades;
|
||||||
|
}, 0);
|
||||||
|
if (totalUnits < tirada) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (container.find('.product').closest('.shipping-addresses-sample')) {
|
||||||
|
if (container.find('.direccion-card').toArray().length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).on('direcciones:actualizadas', '.shipping-addresses-item', function (e) {
|
||||||
|
|
||||||
|
const tirada = $(this).closest('.product').find('.item-tirada').val();
|
||||||
|
const container = $(this);
|
||||||
|
|
||||||
|
if (!checkTotalUnits(container, tirada)) {
|
||||||
|
container.closest('.px-2').find('.btn-add-shipping').removeClass('d-none');
|
||||||
|
} else {
|
||||||
|
container.closest('.px-2').find('.btn-add-shipping').addClass('d-none');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('direcciones:actualizadas', '.shipping-addresses-sample', function (e) {
|
||||||
|
|
||||||
|
const container = $(this);
|
||||||
|
|
||||||
|
if (container.find('.direccion-card').toArray().length === 0) {
|
||||||
|
container.closest('.px-2').find('.btn-add-shipping-sample').removeClass('d-none');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
container.closest('.px-2').find('.btn-add-shipping-sample').addClass('d-none');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$(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.addClass('d-none');
|
||||||
|
seleccionarDireccionEnvio();
|
||||||
|
},
|
||||||
|
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,21 @@
|
|||||||
|
$(() => {
|
||||||
|
|
||||||
|
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';
|
||||||
|
const modalEl = document.getElementById('direccionFormModal');
|
||||||
|
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
export function showLoader() {
|
||||||
|
const el = document.getElementById('sectionLoader');
|
||||||
|
el.classList.remove('d-none');
|
||||||
|
el.classList.add('d-flex');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hideLoader() {
|
||||||
|
const el = document.getElementById('sectionLoader');
|
||||||
|
el.classList.remove('d-flex');
|
||||||
|
el.classList.add('d-none');
|
||||||
|
}
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
<div th:fragment="cartContent(items, cartId)" th:class="${'cart-content container-fluid row gy-4' + (items.isEmpty() ? ' d-none' : '')}">
|
||||||
|
|
||||||
|
<div id="sectionLoader" class="position-absolute top-0 start-0 w-100 h-100 d-none justify-content-center align-items-center
|
||||||
|
bg-body bg-opacity-75" style="z-index:10;">
|
||||||
|
<div class="spinner-border" role="status" style="width:2.5rem;height:2.5rem;">
|
||||||
|
<span class="visually-hidden">Cargando…</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="errorEnvio" th:class="${'alert alert-danger' + (errorEnvio ? '' : ' d-none')}" role="alert"
|
||||||
|
th:text="#{cart.errors.shipping}"></div>
|
||||||
|
<div th:if="${!#strings.isEmpty(errorMessage) and items != null and !items.isEmpty()}" class="alert alert-danger "
|
||||||
|
role="alert" th:text="${errorMessage}"></div>
|
||||||
|
|
||||||
|
<div class="alert alert-danger alert-shipment d-none" role="alert"
|
||||||
|
th:text="#{cart.shipping.errors.fillAddressesItems}"></div>
|
||||||
|
|
||||||
|
<form id="cartForm" th:action="${'/cart/update/' + cart.id}" method="POST" class="col-xl-8 col-12">
|
||||||
|
|
||||||
|
<input type="hidden" name="id" th:value="${cart.id}" />
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<p th:text="#{cart.shipping.info}"></p>
|
||||||
|
<div
|
||||||
|
class="form-check form-switch form-switch-custom form-switch-presupuesto mb-3 d-flex align-items-center">
|
||||||
|
|
||||||
|
<input type="checkbox" class="form-check-input datos-generales-data me-2" id="onlyOneShipment"
|
||||||
|
th:field="${cart.onlyOneShipment}" />
|
||||||
|
<label for="onlyOneShipment" class="form-label d-flex align-items-center mb-0">
|
||||||
|
<span th:text="#{cart.shipping.onlyOneShipment}" class="me-2"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button type="button"
|
||||||
|
th:class="${'btn btn-secondary' + (!cart.onlyOneShipment or #lists.size(mainDir ?: {}) > 0 ? ' d-none' : '')}"
|
||||||
|
id="addOrderAddress" th:text="#{cart.shipping.add}">Añadir dirección</button>
|
||||||
|
|
||||||
|
<div id="shippingAddressesContainer" class="shipping-order-address d-flex flex-wrap gap-3 mt-4">
|
||||||
|
<div th:replace="${cart.onlyOneShipment and mainDir != null}
|
||||||
|
? ~{imprimelibros/direcciones/direccionCard :: direccionCard(
|
||||||
|
${mainDir.direccion},
|
||||||
|
${mainDir.pais},
|
||||||
|
${mainDir.presupuestoId},
|
||||||
|
${mainDir.unidades},
|
||||||
|
${mainDir.isPalets}
|
||||||
|
)}
|
||||||
|
: ~{}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div th:each="item : ${items}" th:insert="~{imprimelibros/cart/_cartItem :: cartItem(${item})}">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div th:replace="~{imprimelibros/cart/_cartSummary :: cartSummary(${cartSummary})}"></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
@ -1,71 +1,192 @@
|
|||||||
<!-- _cartItem.html -->
|
<!-- _cartItem.html -->
|
||||||
<div th:fragment="cartItem(item)" class="card product mb-3 shadow-sm" th:attr="data-iva-4=${item.iva4},
|
<div th:fragment="cartItem(item)" class="card product mb-3 shadow-sm gy-3" th:attr="data-iva-4=${item.iva4},
|
||||||
data-iva-21=${item.iva21},
|
data-iva-21=${item.iva21},
|
||||||
data-base=${item.base}">
|
data-base=${item.base}">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row gy-3">
|
|
||||||
|
|
||||||
<div class="col-sm-auto">
|
<input type="hidden" class="item-presupuesto-id" th:value="${item.presupuestoId}" />
|
||||||
<div class="avatar-lg bg-light rounded p-1">
|
<input type="hidden" class="item-tirada" th:value="${item.tirada}" />
|
||||||
<img th:src="${item.imagen != null ? item.imagen : '/assets/images/products/placeholder.png'}"
|
|
||||||
alt="portada" class="img-fluid d-block rounded">
|
<div class="step-arrow-nav mt-n3 mx-n3 mb-3">
|
||||||
|
<ul th:class="${'nav nav-pills nav-justified custom-nav nav-product' + (cart.onlyOneShipment ? ' d-none' : '')}"
|
||||||
|
role="tablist">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link fs-15 active" th:id="${'pills-details-' + item.cartItemId + '-tab'}"
|
||||||
|
th:data-bs-target="${'#pills-details-' + item.cartItemId}" type="button" role="tab"
|
||||||
|
th:aria-controls="${'#pills-details-' + item.cartItemId}" aria-selected="true"
|
||||||
|
data-bs-toggle="tab">
|
||||||
|
<i
|
||||||
|
class="ri-truck-line fs-5 p-1 bg-soft-primary text-primary rounded-circle align-middle me-2"></i>
|
||||||
|
<label class="fs-13 my-2" th:text="#{cart.tabs.details}">Detalles</label>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link fs-15" th:id="${'pills-shipping-' + item.cartItemId + '-tab'}"
|
||||||
|
th:data-bs-target="${'#pills-shipping-' + item.cartItemId}" type="button" role="tab"
|
||||||
|
th:aria-controls="${'#pills-shipping-' + item.cartItemId}" aria-selected="false"
|
||||||
|
data-bs-toggle="tab">
|
||||||
|
<i
|
||||||
|
class="ri-truck-line fs-5 p-1 bg-soft-primary text-primary rounded-circle align-middle me-2"></i>
|
||||||
|
<label class="fs-13 my-2" th:text="#{cart.tabs.envio}">Envío</label>
|
||||||
|
<i
|
||||||
|
class="ri-error-warning-line fs-5 p-1 bg-soft-danger rounded-circle text-danger align-middle me-2 d-none alert-icon-shipment"></i>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="tab-content">
|
||||||
|
<div class="tab-pane fade show active tab-pane-details" th:id="${'pills-details-' + item.cartItemId}"
|
||||||
|
role="tabpanel" th:aria-labelledby="${'pills-details-' + item.cartItemId + '-tab'}">
|
||||||
|
|
||||||
|
<div class="row g-3 align-items-start">
|
||||||
|
<!-- Col 1: imagen -->
|
||||||
|
<div class="col-auto">
|
||||||
|
<div class="avatar-lg bg-light rounded p-1">
|
||||||
|
<img th:src="${item.imagen != null ? item.imagen : '/assets/images/products/placeholder.png'}"
|
||||||
|
alt="portada" class="img-fluid d-block rounded">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Col 2: detalles -->
|
||||||
|
<div class="col">
|
||||||
|
<h5 class="fs-18 text-truncate mb-1">
|
||||||
|
<a th:href="@{|presupuesto/edit/${item.presupuestoId}|}" class="text-dark"
|
||||||
|
th:text="${item.titulo != null ? item.titulo : 'Presupuesto #'}">Presupuesto</a>
|
||||||
|
</h5>
|
||||||
|
<h5 class="fs-14 text-truncate mb-1">
|
||||||
|
<span th:text="#{cart.item.presupuesto-numero}">Presupuesto #</span>
|
||||||
|
<span th:text="${item.presupuestoId != null ? item.presupuestoId : ''}">#</span>
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<ul class="list-unstyled text-muted mb-1 ps-0">
|
||||||
|
<li th:each="linea : ${item.resumen.lineas}" class="mb-1">
|
||||||
|
<span th:utext="${linea['descripcion']}"></span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul class="list-unstyled text-muted mb-1" th:if="${item.resumen.servicios != null}">
|
||||||
|
<li>
|
||||||
|
<span th:utext="#{pdf.servicios-adicionales}">Servicios adicionales:</span>
|
||||||
|
<span class="spec-label" th:text="${item.resumen.servicios}"></span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul class="list-unstyled text-muted mb-1"
|
||||||
|
th:if="${item.resumen != null and #maps.containsKey(item.resumen,'datosMaquetacion') and item.resumen['datosMaquetacion'] != null}">
|
||||||
|
<li class="spec-row mb-1">
|
||||||
|
<span th:text="#{pdf.datos-maquetacion}">Datos de maquetación:</span>
|
||||||
|
<span th:utext="${item.resumen.datosMaquetacion}"></span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul class="list-unstyled text-muted mb-1"
|
||||||
|
th:if="${item.resumen != null and #maps.containsKey(item.resumen,'datosMarcapaginas') and item.resumen['datosMarcapaginas'] != null}">
|
||||||
|
<li class="spec-row mb-1">
|
||||||
|
<span th:text="#{pdf.datos-marcapaginas}">Datos de marcapáginas:</span>
|
||||||
|
<span th:utext="${item.resumen.datosMarcapaginas}"></span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Col 3: precio -->
|
||||||
|
<div class="col-auto ms-auto text-end">
|
||||||
|
<p class="text-muted mb-1" th:text="#{cart.precio}">Precio</p>
|
||||||
|
<h5 class="fs-14 mb-0">
|
||||||
|
<span th:text="${item.baseTotal != null ? item.baseTotal : '-'}">0,00</span>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Detalles -->
|
<div class="tab-content">
|
||||||
<div class="col-sm">
|
<div class="tab-pane fade show tab-pane-shipping" th:id="${'pills-shipping-' + item.cartItemId}"
|
||||||
<!-- Título / enlace -->
|
role="tabpanel" th:aria-labelledby="${'pills-shipping-' + item.cartItemId + '-tab'}">
|
||||||
<h5 class="fs-18 text-truncate mb-1">
|
|
||||||
<a th:href="@{|presupuesto/edit/${item.presupuestoId}|}" class="text-dark"
|
|
||||||
th:text="${item.titulo != null ? item.titulo : 'Presupuesto #'}">
|
|
||||||
Presupuesto
|
|
||||||
</a>
|
|
||||||
</h5>
|
|
||||||
<h5 class="fs-14 text-truncate mb-1">
|
|
||||||
<span th:text="#{cart.item.presupuesto-numero}">Presupuesto #</span>
|
|
||||||
<span th:text="${item.presupuestoId != null ? item.presupuestoId : ''}">#</span>
|
|
||||||
</h5>
|
|
||||||
|
|
||||||
<!-- Detalles opcionales (ej: cliente, fecha, etc.) -->
|
<div class="col-sm">
|
||||||
<ul class="list-inline text-muted mb-1">
|
<!-- Título / enlace -->
|
||||||
<div th:each="linea : ${item.resumen.lineas}">
|
<h5 class="fs-18 text-truncate my-1 p-1">
|
||||||
<li class="list-inline-item me-3">
|
<a th:href="@{|presupuesto/edit/${item.presupuestoId}|}" class="text-dark"
|
||||||
<div th:utext="${linea['descripcion']}"></div>
|
th:text="${item.titulo != null ? item.titulo : 'Presupuesto #'}">
|
||||||
</li>
|
Presupuesto
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
<h5 class="fs-14 text-truncate mb-1">
|
||||||
|
<span th:text="#{cart.item.presupuesto-numero}">Presupuesto #</span>
|
||||||
|
<span th:text="${item.presupuestoId != null ? item.presupuestoId : ''}">#</span>
|
||||||
|
</h5>
|
||||||
|
<h5 class="fs-14 text-truncate mb-1">
|
||||||
|
<span
|
||||||
|
th:text="#{cart.shipping.tirada} + ' ' + (${item.tirada} != null ? ${item.tirada} : '') + ' ' + #{cart.shipping.unidades} ">
|
||||||
|
</span>
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<div class="card ribbon-box border shadow-none mb-lg-0 material-shadow">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="ribbon ribbon-primary ribbon-shape" th:text="#{checkout.shipping.order}">Envio
|
||||||
|
del pedido
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ribbon-content mt-4">
|
||||||
|
<div class="px-2 mb-2">
|
||||||
|
<button type="button" class="btn btn-secondary btn-add-shipping"
|
||||||
|
th:text="#{cart.shipping.add}">Añadir dirección</button>
|
||||||
|
|
||||||
|
<div class="shipping-addresses-item d-flex flex-wrap gap-3 mt-4">
|
||||||
|
<th:block th:each="dir : ${direcciones}">
|
||||||
|
<th:block
|
||||||
|
th:if="${dir != null and dir.unidades != null and dir.unidades > 0 and dir.presupuestoId == item.presupuestoId}">
|
||||||
|
<div th:replace="~{imprimelibros/direcciones/direccionCard ::
|
||||||
|
direccionCard(
|
||||||
|
direccion=${dir.direccion},
|
||||||
|
pais=${dir.pais},
|
||||||
|
presupuestoId=${dir.presupuestoId},
|
||||||
|
unidades=${dir.unidades},
|
||||||
|
isPalets=${dir.isPalets} )}">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
</th:block>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ul>
|
|
||||||
|
|
||||||
<ul class="list-inline text-muted mb-1" th:if="${item.resumen.servicios != null}">
|
<div class="card ribbon-box border shadow-none mb-lg-0 material-shadow mt-4"
|
||||||
<span th:utext="#{pdf.servicios-adicionales}">Servicios adicionales</span>
|
th:if="${item.hasSample}">
|
||||||
<span class="spec-label" th:text="${item.resumen.servicios}"></span>
|
<div class="card-body">
|
||||||
</ul>
|
<div class="ribbon ribbon-primary ribbon-shape" th:text="#{cart.shipping.samples}">Envio
|
||||||
|
de pruebas
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ul class="list-inline text-muted mb-1" th:if="${item.resumen != null
|
<div class="ribbon-content mt-4">
|
||||||
and #maps.containsKey(item.resumen, 'datosMaquetacion')
|
<div class="px-2 mb-2">
|
||||||
and item.resumen['datosMaquetacion'] != null}">
|
<button type="button" class="btn btn-secondary btn-add-shipping-sample"
|
||||||
<li class="list-inline-item spec-row mb-1">
|
th:text="#{cart.shipping.add}">Añadir dirección</button>
|
||||||
<span th:text="#{pdf.datos-maquetacion}">Datos de maquetación:</span>
|
|
||||||
<span th:utext="${item.resumen.datosMaquetacion}"></span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<ul class="list-inline text-muted mb-1" th:if="${item.resumen != null
|
<div class="shipping-addresses-sample d-flex flex-wrap gap-3 mt-4">
|
||||||
and #maps.containsKey(item.resumen, 'datosMarcapaginas')
|
<th:block th:each="dir : ${direcciones}">
|
||||||
and item.resumen['datosMarcapaginas'] != null}">
|
<th:block
|
||||||
<li class="list-inline-item spec-row mb-1">
|
th:if="${dir != null and dir.unidades == null and dir.presupuestoId == item.presupuestoId}">
|
||||||
<span th:text="#{pdf.datos-marcapaginas}">Datos de marcapáginas:</span>
|
<div th:replace="~{imprimelibros/direcciones/direccionCard ::
|
||||||
<span th:utext="${item.resumen.datosMarcapaginas}"></span>
|
direccionCard(
|
||||||
</li>
|
direccion=${dir.direccion},
|
||||||
</ul>
|
pais=${dir.pais},
|
||||||
|
presupuestoId=${dir.presupuestoId},
|
||||||
|
unidades=${dir.unidades},
|
||||||
|
isPalets=${dir.isPalets} )}">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
</th:block>
|
||||||
<!-- Precio o totales (si los tienes) -->
|
</th:block>
|
||||||
<div class="col-sm-auto text-end">
|
</div>
|
||||||
<p class="text-muted mb-1" th:text="#{cart.precio}">Precio</p>
|
</div>
|
||||||
<h5 class="fs-14 mb-0">
|
</div>
|
||||||
<span th:text="${item.baseTotal != null ? item.baseTotal : '-'}">0,00</span>
|
</div>
|
||||||
</h5>
|
</div>
|
||||||
|
<div class="col-sm-auto div-shipping-product">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -77,7 +198,7 @@
|
|||||||
<!-- Botón eliminar -->
|
<!-- Botón eliminar -->
|
||||||
<div>
|
<div>
|
||||||
<a href="javascript:void(0);" class="d-block text-body p-1 px-2 delete-item"
|
<a href="javascript:void(0);" class="d-block text-body p-1 px-2 delete-item"
|
||||||
th:attr="data-cart-item-id=${item.cartItemId}">
|
th:attr="data-cart-item-id=${item.presupuestoId}">
|
||||||
<i class="ri-delete-bin-fill text-muted align-bottom me-1"></i> Eliminar
|
<i class="ri-delete-bin-fill text-muted align-bottom me-1"></i> Eliminar
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -0,0 +1,76 @@
|
|||||||
|
<div th:fragment="cartSummary(summary)" class="col-xl-4 cart-summary-container">
|
||||||
|
<div class="sticky-side-div">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header border-bottom-dashed">
|
||||||
|
<h5 th:text="#{cart.resumen.title}" class="card-title mb-0"></h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body pt-2">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-borderless mb-0">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><span th:text="#{cart.resumen.base}"></span></td>
|
||||||
|
<td class="text-end" id="base-cesta" th:text="${summary.base}"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><span th:text="#{cart.resumen.envio}"></span></td>
|
||||||
|
<td class="text-end" id="envio-cesta" th:text="${summary.shipment}"></td>
|
||||||
|
</tr>
|
||||||
|
<tr id="tr-iva-4">
|
||||||
|
<td><span th:text="#{cart.resumen.iva-4}"></span> : </td>
|
||||||
|
<td class="text-end" id="iva-4-cesta" th:text="${summary.iva4}"></td>
|
||||||
|
</tr>
|
||||||
|
<tr id="tr-iva-21">
|
||||||
|
<td><span th:text="#{cart.resumen.iva-21}"></span> : </td>
|
||||||
|
<td class="text-end" id="iva-21-cesta" th:text="${summary.iva21}"></td>
|
||||||
|
</tr>
|
||||||
|
<tr id="tr-iva-21">
|
||||||
|
<td><span
|
||||||
|
th:text="#{cart.resumen.descuento} + ' (' + ${summary.fidelizacion} + ')'"></span>
|
||||||
|
: </td>
|
||||||
|
<td class="text-end" id="descuento-cesta" th:text="${summary.descuento}"></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="table-active">
|
||||||
|
<th><span th:text="#{cart.resumen.total}"></span>:</th>
|
||||||
|
<td class="text-end">
|
||||||
|
<span id="total-cesta" class="fw-semibold" th:text="${summary.total}"></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<form th:action="@{/pagos/redsys/crear}" method="post">
|
||||||
|
<input type="hidden" name="order" value="123456789012" />
|
||||||
|
<input type="hidden" name="amountCents" value="12525" />
|
||||||
|
<button id="btn-checkout" type="submit" class="btn btn-secondary w-100 mt-2"
|
||||||
|
th:text="#{cart.resumen.tramitar}">Checkout</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<!-- end table-responsive -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div sec:authorize="isAuthenticated() and hasAnyRole('SUPERADMIN','ADMIN')" class="card">
|
||||||
|
<div class="card-header border-bottom-dashed">
|
||||||
|
<h5 th:text="#{cart.pass-to.customer}" class="card-title mb-0"></h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body pt-2">
|
||||||
|
<div class="alert alert-info" role="alert" th:text="#{cart.pass-to.customer.info}"></div>
|
||||||
|
<div class="alert alert-warning" role="alert" th:text="#{cart.pass-to.customer.warning}"></div>
|
||||||
|
<div id="alert-select-customer" class="alert alert-danger d-none" role="alert"
|
||||||
|
th:text="#{cart.pass-to.customer.error}"></div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="select-customer" class="form-label" th:text="#{cart.pass-to.select-customer}"></label>
|
||||||
|
<select id="select-customer" name="customerId" class="form-select" required>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<button id="moveCart" class="btn btn-secondary w-100" th:text="#{cart.pass-to.button}">Mover
|
||||||
|
cesta</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- end stickey -->
|
||||||
|
|
||||||
|
</div>
|
||||||
@ -9,7 +9,7 @@
|
|||||||
<link th:href="@{/assets/libs/datatables/dataTables.bootstrap5.min.css}" rel="stylesheet" />
|
<link th:href="@{/assets/libs/datatables/dataTables.bootstrap5.min.css}" rel="stylesheet" />
|
||||||
</th:block>
|
</th:block>
|
||||||
<th:block layout:fragment="pagecss">
|
<th:block layout:fragment="pagecss">
|
||||||
<link th:href="@{/assets/css/presupuestador.css}" rel="stylesheet" />
|
<link th:href="@{/assets/css/cart.css}" rel="stylesheet" />
|
||||||
</th:block>
|
</th:block>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@ -22,6 +22,10 @@
|
|||||||
<th:block layout:fragment="content">
|
<th:block layout:fragment="content">
|
||||||
<div th:if="${#authorization.expression('isAuthenticated()')}">
|
<div th:if="${#authorization.expression('isAuthenticated()')}">
|
||||||
|
|
||||||
|
<div
|
||||||
|
th:replace="imprimelibros/partials/modal-form :: modal('direccionFormModal', 'direcciones.add', 'modal-md', 'direccionFormModalBody')">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<nav aria-label="breadcrumb">
|
<nav aria-label="breadcrumb">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
@ -32,67 +36,12 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container-fluid row gy-4">
|
<div th:if="${items.isEmpty()}">
|
||||||
<div th:if="${items.isEmpty()}">
|
<div id="alert-empty"class="alert alert-info" role="alert" th:text="#{cart.empty}"></div>
|
||||||
<div class="alert alert-info" role="alert" th:text="#{cart.empty}"></div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xl-8 col-12">
|
|
||||||
<div th:each="item : ${items}" th:insert="~{imprimelibros/cart/_cartItem :: cartItem(${item})}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-xl-4">
|
|
||||||
<div class="sticky-side-div">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header border-bottom-dashed">
|
|
||||||
<h5 th:text="#{cart.resumen.title}" class="card-title mb-0"></h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body pt-2">
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-borderless mb-0">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td><span th:text="#{cart.resumen.base}"></span></td>
|
|
||||||
<td class="text-end" id="base-cesta"></td>
|
|
||||||
</tr>
|
|
||||||
<tr id="tr-iva-4">
|
|
||||||
<td><span th:text="#{cart.resumen.iva-4}"></span> : </td>
|
|
||||||
<td class="text-end" id="iva-4-cesta"></td>
|
|
||||||
</tr>
|
|
||||||
<tr id="tr-iva-21">
|
|
||||||
<td><span th:text="#{cart.resumen.iva-21}"></span> : </td>
|
|
||||||
<td class="text-end" id="iva-21-cesta"></td>
|
|
||||||
</tr>
|
|
||||||
<tr class="table-active">
|
|
||||||
<th><span th:text="#{cart.resumen.total}"></span>:</th>
|
|
||||||
<td class="text-end">
|
|
||||||
<span id="total-cesta" class="fw-semibold">
|
|
||||||
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<button type="button" class="btn btn-secondary w-100 mt-2"
|
|
||||||
th:text="#{cart.resumen.tramitar}">Checkout</button>
|
|
||||||
</div>
|
|
||||||
<!-- end table-responsive -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="alert border-dashed alert-danger" role="alert">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="ms-2">
|
|
||||||
<h5 class="fs-14 text-danger fw-semibold" th:text="#{cart.resumen.fidelizacion}"></h5>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- end stickey -->
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div th:insert="~{imprimelibros/cart/_cartContent :: cartContent(${items}, ${cartId})}"></div>
|
||||||
|
|
||||||
</th:block>
|
</th:block>
|
||||||
|
|
||||||
<th:block th:replace="~{theme/partials/vendor-scripts :: scripts}" />
|
<th:block th:replace="~{theme/partials/vendor-scripts :: scripts}" />
|
||||||
@ -102,6 +51,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script type="module" th:src="@{/assets/js/pages/imprimelibros/cart/cart.js}"></script>
|
<script type="module" th:src="@{/assets/js/pages/imprimelibros/cart/cart.js}"></script>
|
||||||
|
<script type="module" th:src="@{/assets/js/pages/imprimelibros/cart/shipping-cart.js}"></script>
|
||||||
</th:block>
|
</th:block>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,43 @@
|
|||||||
|
<div>
|
||||||
|
<div
|
||||||
|
th:replace="imprimelibros/partials/modal-form :: modal('direccionFormModal', 'direcciones.add', 'modal-md', 'direccionFormModalBody')">
|
||||||
|
</div>
|
||||||
|
<div class="card ribbon-box border shadow-none mb-lg-0 material-shadow">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="ribbon ribbon-primary ribbon-shape" th:text="#{checkout.shipping.order}">Envio del pedido
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ribbon-content mt-4">
|
||||||
|
<div class="px-2 mb-2">
|
||||||
|
<p th:text="#{checkout.shipping.info}"></p>
|
||||||
|
<div
|
||||||
|
class="form-check form-switch form-switch-custom form-switch-presupuesto mb-3 d-flex align-items-center">
|
||||||
|
<input type="checkbox" class="form-check-input datos-generales-data me-2" id="onlyOneShipment"
|
||||||
|
name="onlyOneShipment" checked />
|
||||||
|
<label for="onlyOneShipment" class="form-label d-flex align-items-center mb-0">
|
||||||
|
<span th:text="#{checkout.shipping.onlyOneShipment}" class="me-2"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-secondary" id="addOrderAddress"
|
||||||
|
th:text="#{checkout.shipping.add}">Añadir dirección</button>
|
||||||
|
|
||||||
|
<div id="orderShippingAddressesContainer" class="mt-4"></div>
|
||||||
|
<div id="orderShippingMultipleAddressesContainer d-none" class="mt-4"></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card ribbon-box border shadow-none mb-lg-0 material-shadow mt-4" th:if="${hasSample}">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="ribbon ribbon-primary ribbon-shape" th:text="#{checkout.shipping.samples}">Envio de pruebas
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ribbon-content mt-4">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- End Ribbon Shape -->
|
||||||
|
|
||||||
|
</div>
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
<div>
|
||||||
|
|
||||||
|
</div>
|
||||||
@ -0,0 +1,175 @@
|
|||||||
|
<!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/libs/datatables/dataTables.bootstrap5.min.css}" rel="stylesheet" />
|
||||||
|
</th:block>
|
||||||
|
<th:block layout:fragment="pagecss">
|
||||||
|
<link th:href="@{/assets/css/presupuestador.css}" rel="stylesheet" />
|
||||||
|
</th:block>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div th:replace="~{imprimelibros/partials/topbar :: topbar}" />
|
||||||
|
<div th:replace="~{imprimelibros/partials/sidebar :: sidebar}"
|
||||||
|
sec:authorize="isAuthenticated() and hasAnyRole('SUPERADMIN','ADMIN')">
|
||||||
|
|
||||||
|
<th:block layout:fragment="content">
|
||||||
|
<div th:if="${#authorization.expression('isAuthenticated()')}">
|
||||||
|
|
||||||
|
<div class="container-fluid">
|
||||||
|
<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="#{checkout.title}">Finalizar
|
||||||
|
compra</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xl-8 col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="step-arrow-nav mt-n3 mx-n3 mb-3">
|
||||||
|
|
||||||
|
<ul class="nav nav-pills nav-justified custom-nav" role="tablist">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link fs-15 p-3 active" id="pills-shipping-tab"
|
||||||
|
data-bs-target="#pills-shipping" type="button" role="tab"
|
||||||
|
aria-controls="pills-shipping" aria-selected="true">
|
||||||
|
<i
|
||||||
|
class="ri-truck-line fs-5 p-1 bg-soft-primary text-primary rounded-circle align-middle me-2"></i>
|
||||||
|
<label class="fs-13 my-2" th:text="#{checkout.shipping}">Envío</label>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link fs-15 p-3" id="pills-payment-tab"
|
||||||
|
data-bs-target="#pills-payment" type="button" role="tab"
|
||||||
|
aria-controls="pills-payment" aria-selected="false">
|
||||||
|
<i
|
||||||
|
class="ri-money-euro-box-line fs-5 p-1 bg-soft-primary text-primary rounded-circle align-middle me-2"></i>
|
||||||
|
<label class="fs-13 my-2" th:text="#{checkout.payment}">Método de
|
||||||
|
pago</label>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="tab-content">
|
||||||
|
<div class="tab-pane fade show active" id="pills-shipping" role="tabpanel"
|
||||||
|
aria-labelledby="pills-shipping-tab">
|
||||||
|
|
||||||
|
<div th:include="~{imprimelibros/checkout/_envio.html}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- end tab pane -->
|
||||||
|
|
||||||
|
<div class="tab-pane fade" id="pills-payment" role="tabpanel"
|
||||||
|
aria-labelledby="pills-payment-tab">
|
||||||
|
|
||||||
|
<div th:include="~{imprimelibros/checkout/_pago.html}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- end tab pane -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-xl-4">
|
||||||
|
<div class="sticky-side-div">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header border-bottom-dashed">
|
||||||
|
<h5 th:text="#{checkout.summay}" class="card-title mb-1"></h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body pt-2">
|
||||||
|
<div class="table-responsive table-card">
|
||||||
|
<table class="table table-borderless align-middle mb-0">
|
||||||
|
<thead class="table-light text-muted">
|
||||||
|
<tr>
|
||||||
|
<th style="width: 90px;" scope="col"
|
||||||
|
th:text="#{checkout.summary.presupuesto}">Presupuesto</th>
|
||||||
|
<th scope="col" th:text="#{checkout.summary.titulo}">Título</th>
|
||||||
|
<th scope="col" class="text-end" th:text="#{checkout.summary.base}">
|
||||||
|
Base</th>
|
||||||
|
<th class="d-none"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr th:each="item : ${items}">
|
||||||
|
<td>
|
||||||
|
<span th:text="${item.presupuestoId}">PRESUPUESTO-001</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span th:text="${item.titulo}">Título del presupuesto</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<span th:text="${item.baseTotal}">
|
||||||
|
0,00</span>
|
||||||
|
</td>
|
||||||
|
<td class="d-none">
|
||||||
|
<span th:text="${item.tirada}"></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2"><span th:text="#{cart.resumen.base}"></span></td>
|
||||||
|
<td class="text-end" id="base-cesta"></td>
|
||||||
|
</tr>
|
||||||
|
<tr id="tr-iva-4">
|
||||||
|
<td colspan="2"><span th:text="#{cart.resumen.iva-4}"></span> : </td>
|
||||||
|
<td class="text-end" id="iva-4-cesta"></td>
|
||||||
|
</tr>
|
||||||
|
<tr id="tr-iva-21">
|
||||||
|
<td colspan="2"><span th:text="#{cart.resumen.iva-21}"></span> : </td>
|
||||||
|
<td class="text-end" id="iva-21-cesta"></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="table-active">
|
||||||
|
<td colspan="2"><span th:text="#{cart.resumen.total}"></span>:</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<span id="total-cesta" class="fw-semibold">
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
<button type="button" class="btn btn-secondary w-100 mt-2"
|
||||||
|
th:text="#{cart.resumen.tramitar}">Checkout</button>
|
||||||
|
</div>
|
||||||
|
<!-- end table-responsive -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert border-dashed alert-danger" role="alert">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="ms-2">
|
||||||
|
<h5 class="fs-14 text-danger fw-semibold"
|
||||||
|
th:text="#{cart.resumen.fidelizacion}"></h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- end stickey -->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</th:block>
|
||||||
|
|
||||||
|
<th:block th:replace="~{theme/partials/vendor-scripts :: scripts}" />
|
||||||
|
<th:block layout:fragment="pagejs">
|
||||||
|
<script th:inline="javascript">
|
||||||
|
window.languageBundle = /*[[${languageBundle}]]*/ {};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="module" th:src="@{/assets/js/pages/imprimelibros/checkout/checkout.js}"></script>
|
||||||
|
</th:block>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -0,0 +1,169 @@
|
|||||||
|
<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>
|
||||||
|
|
||||||
|
<input 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>
|
||||||
|
<span class="text-danger">*</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>
|
||||||
@ -103,6 +103,7 @@
|
|||||||
<div class="form-group mt-2">
|
<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>
|
||||||
|
<span class="text-danger">*</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"
|
||||||
th:classappend="${#fields.hasErrors('telefono')} ? ' is-invalid'">
|
th:classappend="${#fields.hasErrors('telefono')} ? ' is-invalid'">
|
||||||
|
|||||||
@ -0,0 +1,52 @@
|
|||||||
|
<div th:fragment="direccionCard(direccion, pais, presupuestoId, unidades, isPalets)" name="direccion"
|
||||||
|
class="card card border mb-3 direccion-card bg-light w-100 mx-2">
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<input type="hidden" class="presupuesto-id" th:value="${presupuestoId}" />
|
||||||
|
<input type="hidden" class="direccion-id" th:value="${direccion.id}" />
|
||||||
|
<input type="hidden" class="direccion-cp" th:value="${direccion.cp}" />
|
||||||
|
<input type="hidden" class="direccion-pais-code3" th:value="${direccion.pais.code3}" />
|
||||||
|
<input type="hidden" class="item-tirada" th:value="${unidades != null ? unidades : ''}" />
|
||||||
|
<input type="hidden" class="is-palets" th:value="${isPalets != null ? isPalets : ''}" />
|
||||||
|
|
||||||
|
<div class="row g-3 align-items-start flex-nowrap">
|
||||||
|
<div class="col">
|
||||||
|
<span class="mb-2 fw-semibold d-block text-muted text-uppercase text-break" th:text="${direccion.alias}"></span>
|
||||||
|
<span class="fs-14 mb-1 d-block text-break" th:text="${direccion.att}"></span>
|
||||||
|
<span class="text-muted fw-normal text-wrap mb-1 d-block text-break" th:text="${direccion.direccion}"></span>
|
||||||
|
<span class="text-muted fw-normal d-block text-break" th:text="${pais}"></span>
|
||||||
|
<span class="text-muted fw-normal d-block text-break"
|
||||||
|
th:text="#{'direcciones.telefono'} + ': ' + ${direccion.telefono}"></span>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto ms-auto text-end">
|
||||||
|
<div th:if="${unidades != null}">
|
||||||
|
<span id="units-text" class="mb-2 fw-semibold d-block text-muted text-uppercase" th:if="${unidades == 1}"
|
||||||
|
th:text="|${unidades} #{cart.shipping.ud}|"></span>
|
||||||
|
|
||||||
|
<!-- plural -->
|
||||||
|
<span id="units-text" class="mb-2 fw-semibold d-block text-muted text-uppercase" th:unless="${unidades == 1}"
|
||||||
|
th:text="|${unidades} #{cart.shipping.uds}|"></span>
|
||||||
|
</div>
|
||||||
|
<div th:if="${isPalets != null and isPalets==1}">
|
||||||
|
<i class="icon-shipment las la-pallet la-3x text-muted"></i>
|
||||||
|
</div>
|
||||||
|
<div th:if="${isPalets == null or isPalets == 0}">
|
||||||
|
<i class="icon-shipment las la-box la-3x text-muted"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-wrap align-items-center gap-2 py-1 bg-light rounded-bottom border-top mt-auto actions-row">
|
||||||
|
<a href="javascript:void(0)" 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>
|
||||||
|
<span th:text="#{'direcciones.btn.delete'}">Eliminar</span>
|
||||||
|
</a>
|
||||||
|
<a th:if="${unidades != null}" href="javascript:void(0)" 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>
|
||||||
|
<span th:text="#{'direcciones.btn.edit'}">Editar</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
@ -6,6 +6,9 @@
|
|||||||
<head>
|
<head>
|
||||||
<!--page title-->
|
<!--page title-->
|
||||||
<th:block layout:fragment="pagetitle" />
|
<th:block layout:fragment="pagetitle" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||||
|
<meta http-equiv="Pragma" content="no-cache">
|
||||||
|
<meta http-equiv="Expires" content="0">
|
||||||
|
|
||||||
<!-- Page CSS -->
|
<!-- Page CSS -->
|
||||||
<th:block th:replace="~{imprimelibros/partials/head-css :: head-css}" />
|
<th:block th:replace="~{imprimelibros/partials/head-css :: head-css}" />
|
||||||
|
|||||||
@ -0,0 +1,7 @@
|
|||||||
|
<form id="tpv" th:action="${action}" method="POST">
|
||||||
|
<input type="hidden" name="Ds_SignatureVersion" th:value="${signatureVersion}" />
|
||||||
|
<input type="hidden" name="Ds_MerchantParameters" th:value="${merchantParameters}" />
|
||||||
|
<input type="hidden" name="Ds_Signature" th:value="${signature}" />
|
||||||
|
<noscript><button type="submit">Pagar</button></noscript>
|
||||||
|
</form>
|
||||||
|
<script>document.getElementById('tpv').submit();</script>
|
||||||
44
src/test/java/com/imprimelibros/erp/calcularEnvios.java
Normal file
44
src/test/java/com/imprimelibros/erp/calcularEnvios.java
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package com.imprimelibros.erp;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import com.imprimelibros.erp.externalApi.skApiClient;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
public class calcularEnvios {
|
||||||
|
@Autowired
|
||||||
|
private skApiClient apiClient;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPrecioCalculadoDevuelveJson() {
|
||||||
|
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
data.put("pais_code3", "esp");
|
||||||
|
data.put("cp", 18200);
|
||||||
|
data.put("peso", 7.82);
|
||||||
|
data.put("unidades", 10);
|
||||||
|
data.put("palets", 0);
|
||||||
|
|
||||||
|
|
||||||
|
// get locale
|
||||||
|
Locale locale = Locale.forLanguageTag("es-ES");
|
||||||
|
|
||||||
|
|
||||||
|
Map<String, Object> resultado = apiClient.getCosteEnvio(data, locale);
|
||||||
|
|
||||||
|
System.out.println("📦 Resultado de la API:");
|
||||||
|
System.out.println(resultado);
|
||||||
|
|
||||||
|
assertNotNull(resultado, "El resultado no debe ser null");
|
||||||
|
/*assertTrue(resultado.trim().startsWith("{"), "El resultado debe comenzar con { (JSON)");
|
||||||
|
assertTrue(resultado.trim().endsWith("}"), "El resultado debe terminar con } (JSON)");*/
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user