mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-01-12 16:38:48 +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>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.5.6</version>
|
||||
<version>3.5.7</version>
|
||||
<relativePath /> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>com.imprimelibros</groupId>
|
||||
@ -151,6 +151,41 @@
|
||||
<version>${liquibase.version}</version>
|
||||
</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>
|
||||
|
||||
<build>
|
||||
|
||||
@ -1,18 +1,23 @@
|
||||
package com.imprimelibros.erp.cart;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Entity
|
||||
@Table(name = "carts",
|
||||
uniqueConstraints = @UniqueConstraint(name="uq_carts_user_active", columnNames={"user_id","status"}))
|
||||
@Table(name = "carts", uniqueConstraints = @UniqueConstraint(name = "uq_carts_user_active", columnNames = { "user_id",
|
||||
"status" }))
|
||||
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;
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
@ -25,6 +30,9 @@ public class Cart {
|
||||
@Column(nullable = false, length = 3)
|
||||
private String currency = "EUR";
|
||||
|
||||
@Column(name = "only_one_shipment", nullable = false)
|
||||
private Boolean onlyOneShipment = true;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private LocalDateTime createdAt = LocalDateTime.now();
|
||||
|
||||
@ -34,23 +42,93 @@ public class Cart {
|
||||
@OneToMany(mappedBy = "cart", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
|
||||
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
|
||||
public void preUpdate() { this.updatedAt = LocalDateTime.now(); }
|
||||
public void preUpdate() {
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
// Getters & Setters
|
||||
public Long getId() { return id; }
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public Status getStatus() { return status; }
|
||||
public void setStatus(Status status) { this.status = status; }
|
||||
public Long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public String getCurrency() { return currency; }
|
||||
public void setCurrency(String currency) { this.currency = currency; }
|
||||
public void setUserId(Long userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public Status getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public List<CartItem> getItems() { return items; }
|
||||
public void setItems(List<CartItem> items) { this.items = items; }
|
||||
public void setStatus(Status status) {
|
||||
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.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import com.imprimelibros.erp.users.UserDetailsImpl;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
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.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.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import com.imprimelibros.erp.cart.dto.UpdateCartRequest;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/cart")
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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");
|
||||
this.direccionService = direccionService;
|
||||
this.messageSource = messageSource;
|
||||
this.translationService = translationService;
|
||||
}
|
||||
|
||||
/** Vista del carrito */
|
||||
@GetMapping
|
||||
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);
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/** Añadir presupuesto via POST form */
|
||||
@PostMapping("/add")
|
||||
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";
|
||||
}
|
||||
|
||||
/** Añadir presupuesto con ruta REST (opcional) */
|
||||
@PostMapping("/add/{presupuestoId}")
|
||||
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"));
|
||||
if (isAjax) {
|
||||
// Responder 200 con la URL a la que quieres ir
|
||||
@ -83,13 +117,13 @@ public class CartController {
|
||||
public long getCount(Principal principal) {
|
||||
if (principal == null)
|
||||
return 0;
|
||||
return service.countItems(currentUserId(principal));
|
||||
return service.countItems(Utils.currentUserId(principal));
|
||||
}
|
||||
|
||||
/** Eliminar línea por ID de item */
|
||||
@DeleteMapping("/{itemId}/remove")
|
||||
public String remove(@PathVariable Long itemId, Principal principal) {
|
||||
service.removeItem(currentUserId(principal), itemId);
|
||||
service.removeItem(Utils.currentUserId(principal), itemId);
|
||||
return "redirect:/cart";
|
||||
}
|
||||
|
||||
@ -97,14 +131,72 @@ public class CartController {
|
||||
@DeleteMapping("/delete/item/{presupuestoId}")
|
||||
@ResponseBody
|
||||
public String removeByPresupuesto(@PathVariable Long presupuestoId, Principal principal) {
|
||||
service.removeByPresupuesto(currentUserId(principal), presupuestoId);
|
||||
service.removeByPresupuesto(Utils.currentUserId(principal), presupuestoId);
|
||||
return "redirect:/cart";
|
||||
}
|
||||
|
||||
/** Vaciar carrito completo */
|
||||
@DeleteMapping("/clear")
|
||||
public String clear(Principal principal) {
|
||||
service.clear(currentUserId(principal));
|
||||
service.clear(Utils.currentUserId(principal));
|
||||
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 java.time.LocalDateTime;
|
||||
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||
|
||||
@Entity
|
||||
@Table(
|
||||
name = "cart_items",
|
||||
@ -17,8 +19,9 @@ public class CartItem {
|
||||
@JoinColumn(name = "cart_id", nullable = false)
|
||||
private Cart cart;
|
||||
|
||||
@Column(name = "presupuesto_id", nullable = false)
|
||||
private Long presupuestoId;
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "presupuesto_id", nullable = false)
|
||||
private Presupuesto presupuesto;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private LocalDateTime createdAt = LocalDateTime.now();
|
||||
@ -29,8 +32,8 @@ public class CartItem {
|
||||
public Cart getCart() { return cart; }
|
||||
public void setCart(Cart cart) { this.cart = cart; }
|
||||
|
||||
public Long getPresupuestoId() { return presupuestoId; }
|
||||
public void setPresupuestoId(Long presupuestoId) { this.presupuestoId = presupuestoId; }
|
||||
public Presupuesto getPresupuesto() { return presupuesto; }
|
||||
public void setPresupuesto(Presupuesto presupuesto) { this.presupuesto = presupuesto; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
}
|
||||
|
||||
@ -1,9 +1,22 @@
|
||||
package com.imprimelibros.erp.cart;
|
||||
|
||||
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;
|
||||
|
||||
public interface CartRepository extends JpaRepository<Cart, Long> {
|
||||
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;
|
||||
|
||||
import jakarta.transaction.Transactional;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.stereotype.Service;
|
||||
@ -10,32 +10,43 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import com.imprimelibros.erp.presupuesto.classes.PresupuestoFormatter;
|
||||
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.direcciones.DireccionService;
|
||||
import com.imprimelibros.erp.externalApi.skApiClient;
|
||||
import com.imprimelibros.erp.pedido.PedidoService;
|
||||
import com.imprimelibros.erp.presupuesto.PresupuestoRepository;
|
||||
|
||||
|
||||
@Service
|
||||
public class CartService {
|
||||
|
||||
private final CartRepository cartRepo;
|
||||
private final CartItemRepository itemRepo;
|
||||
private final MessageSource messageSource;
|
||||
private final PresupuestoFormatter presupuestoFormatter;
|
||||
private final PresupuestoRepository presupuestoRepo;
|
||||
private final Utils utils;
|
||||
private final DireccionService direccionService;
|
||||
private final skApiClient skApiClient;
|
||||
private final PedidoService pedidoService;
|
||||
|
||||
public CartService(CartRepository cartRepo, CartItemRepository itemRepo,
|
||||
MessageSource messageSource, PresupuestoFormatter presupuestoFormatter,
|
||||
PresupuestoRepository presupuestoRepo, Utils utils) {
|
||||
PresupuestoRepository presupuestoRepo, Utils utils, DireccionService direccionService,
|
||||
skApiClient skApiClient, PedidoService pedidoService) {
|
||||
this.cartRepo = cartRepo;
|
||||
this.itemRepo = itemRepo;
|
||||
this.messageSource = messageSource;
|
||||
this.presupuestoFormatter = presupuestoFormatter;
|
||||
this.presupuestoRepo = presupuestoRepo;
|
||||
this.utils = utils;
|
||||
this.direccionService = direccionService;
|
||||
this.skApiClient = skApiClient;
|
||||
this.pedidoService = pedidoService;
|
||||
}
|
||||
|
||||
/** 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. */
|
||||
@Transactional
|
||||
public List<Map<String, Object>> listItems(Long userId, Locale locale) {
|
||||
@ -58,14 +74,13 @@ public class CartService {
|
||||
List<CartItem> items = itemRepo.findByCartId(cart.getId());
|
||||
for (CartItem item : items) {
|
||||
|
||||
Presupuesto p = presupuestoRepo.findById(item.getPresupuestoId())
|
||||
.orElseThrow(() -> new IllegalStateException("Presupuesto no encontrado: " + item.getPresupuestoId()));
|
||||
Presupuesto p = item.getPresupuesto();
|
||||
|
||||
Map<String, Object> elemento = getElementoCart(p, locale);
|
||||
elemento.put("cartItemId", item.getId());
|
||||
resultados.add(elemento);
|
||||
}
|
||||
//System.out.println("Cart items: " + resultados);
|
||||
// System.out.println("Cart items: " + resultados);
|
||||
return resultados;
|
||||
}
|
||||
|
||||
@ -77,7 +92,8 @@ public class CartService {
|
||||
if (!exists) {
|
||||
CartItem ci = new CartItem();
|
||||
ci.setCart(cart);
|
||||
ci.setPresupuestoId(presupuestoId);
|
||||
ci.setPresupuesto(presupuestoRepo.findById(presupuestoId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Presupuesto no encontrado")));
|
||||
itemRepo.save(ci);
|
||||
}
|
||||
}
|
||||
@ -96,8 +112,9 @@ public class CartService {
|
||||
@Transactional
|
||||
public void removeByPresupuesto(Long userId, Long presupuestoId) {
|
||||
Cart cart = getOrCreateActiveCart(userId);
|
||||
itemRepo.findByCartIdAndPresupuestoId(cart.getId(), presupuestoId)
|
||||
.ifPresent(itemRepo::delete);
|
||||
CartItem item = itemRepo.findByCartIdAndPresupuestoId(cart.getId(), presupuestoId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Item no encontrado"));
|
||||
itemRepo.deleteById(item.getId());
|
||||
}
|
||||
|
||||
/** Vacía todo el carrito activo. */
|
||||
@ -122,9 +139,9 @@ public class CartService {
|
||||
}
|
||||
|
||||
private Map<String, Object> getElementoCart(Presupuesto presupuesto, Locale locale) {
|
||||
|
||||
|
||||
Map<String, Object> resumen = new HashMap<>();
|
||||
|
||||
|
||||
resumen.put("titulo", presupuesto.getTitulo());
|
||||
|
||||
resumen.put("imagen",
|
||||
@ -134,8 +151,15 @@ public class CartService {
|
||||
|
||||
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);
|
||||
|
||||
resumen.put("tirada", presupuesto.getSelectedTirada());
|
||||
|
||||
resumen.put("baseTotal", Utils.formatCurrency(presupuesto.getBaseImponible(), locale));
|
||||
resumen.put("base", presupuesto.getBaseImponible());
|
||||
resumen.put("iva4", presupuesto.getIvaImporte4());
|
||||
@ -145,4 +169,257 @@ public class CartService {
|
||||
|
||||
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.RoundingMode;
|
||||
import java.security.Principal;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
@ -12,6 +13,8 @@ import java.util.Optional;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
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.maquetacion.MaquetacionMatrices;
|
||||
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.Path;
|
||||
@ -40,6 +45,30 @@ public class Utils {
|
||||
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) {
|
||||
NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance(locale);
|
||||
return currencyFormatter.format(amount);
|
||||
|
||||
@ -91,7 +91,6 @@ public class SecurityConfig {
|
||||
// Ignora CSRF para tu recurso público (sin Ant/Mvc matchers)
|
||||
.csrf(csrf -> csrf
|
||||
.ignoringRequestMatchers(pathStartsWith("/presupuesto/public/")))
|
||||
|
||||
// ====== RequestCache: sólo navegaciones HTML reales ======
|
||||
.requestCache(rc -> {
|
||||
HttpSessionRequestCache cache = new HttpSessionRequestCache();
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package com.imprimelibros.erp.direcciones;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@ -43,20 +44,23 @@ import jakarta.validation.Valid;
|
||||
@RequestMapping("/direcciones")
|
||||
public class DireccionController {
|
||||
|
||||
private final DireccionService direccionService;
|
||||
|
||||
protected final DireccionRepository repo;
|
||||
protected final PaisesService paisesService;
|
||||
protected final MessageSource messageSource;
|
||||
protected final UserDao userRepo;
|
||||
protected final TranslationService translationService;
|
||||
|
||||
|
||||
public DireccionController(DireccionRepository repo, PaisesService paisesService,
|
||||
MessageSource messageSource, UserDao userRepo, TranslationService translationService) {
|
||||
MessageSource messageSource, UserDao userRepo, TranslationService translationService,
|
||||
DireccionService direccionService) {
|
||||
this.repo = repo;
|
||||
this.paisesService = paisesService;
|
||||
this.messageSource = messageSource;
|
||||
this.userRepo = userRepo;
|
||||
this.translationService = translationService;
|
||||
this.direccionService = direccionService;
|
||||
}
|
||||
|
||||
@GetMapping()
|
||||
@ -295,6 +299,33 @@ public class DireccionController {
|
||||
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
|
||||
public String create(
|
||||
@Valid @ModelAttribute("dirForm") Direccion direccion,
|
||||
@ -327,6 +358,34 @@ public class DireccionController {
|
||||
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}")
|
||||
public String update(
|
||||
@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) {
|
||||
if (auth == null) {
|
||||
return false;
|
||||
}
|
||||
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) {
|
||||
return true;
|
||||
}
|
||||
@ -434,4 +517,5 @@ public class DireccionController {
|
||||
}
|
||||
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.web.client.HttpClientErrorException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
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 java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.net.URI;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
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
|
||||
******************/
|
||||
@ -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(
|
||||
BigDecimal importe, BigDecimal importeMin, BigDecimal importeMax,
|
||||
BigDecimal margenMax, BigDecimal margenMin) {
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
package com.imprimelibros.erp.pdf;
|
||||
|
||||
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.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.ui.Model;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
@ -621,14 +622,14 @@ public class PresupuestoController {
|
||||
@ResponseBody
|
||||
public DataTablesResponse<Map<String, Object>> datatable(
|
||||
HttpServletRequest request, Authentication auth, Locale locale,
|
||||
@PathVariable("tipo") String tipo) {
|
||||
@PathVariable("tipo") String tipo, Principal principal) {
|
||||
|
||||
DataTablesRequest dt = DataTablesParser.from(request);
|
||||
|
||||
if ("anonimos".equals(tipo)) {
|
||||
return dtService.datatablePublicos(dt, locale);
|
||||
return dtService.datatablePublicos(dt, locale, principal);
|
||||
} else if ("clientes".equals(tipo)) {
|
||||
return dtService.datatablePrivados(dt, locale);
|
||||
return dtService.datatablePrivados(dt, locale, principal);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Tipo de datatable no válido");
|
||||
}
|
||||
|
||||
@ -1,15 +1,18 @@
|
||||
package com.imprimelibros.erp.presupuesto;
|
||||
|
||||
import com.imprimelibros.erp.common.Utils;
|
||||
import com.imprimelibros.erp.configuracion.margenes_presupuestos.MargenPresupuesto;
|
||||
import com.imprimelibros.erp.datatables.*;
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||
|
||||
import jakarta.persistence.criteria.Expression;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.*;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
@ -26,18 +29,29 @@ public class PresupuestoDatatableService {
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public DataTablesResponse<Map<String, Object>> datatablePublicos(DataTablesRequest dt, Locale locale) {
|
||||
return commonDataTable(dt, locale, "publico", true);
|
||||
public DataTablesResponse<Map<String, Object>> datatablePublicos(DataTablesRequest dt, Locale locale,
|
||||
Principal principal) {
|
||||
return commonDataTable(dt, locale, "publico", true, principal);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public DataTablesResponse<Map<String, Object>> datatablePrivados(DataTablesRequest dt, Locale locale) {
|
||||
return commonDataTable(dt, locale, "privado", false);
|
||||
public DataTablesResponse<Map<String, Object>> datatablePrivados(DataTablesRequest dt, Locale locale,
|
||||
Principal principal) {
|
||||
return commonDataTable(dt, locale, "privado", false, principal);
|
||||
}
|
||||
|
||||
private DataTablesResponse<Map<String, Object>> commonDataTable(DataTablesRequest dt, Locale locale, String origen,
|
||||
boolean publico) {
|
||||
Long count = repo.findAllByOrigen(Presupuesto.Origen.valueOf(origen)).stream().count();
|
||||
boolean publico, Principal principal) {
|
||||
|
||||
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(
|
||||
"id", "titulo", "user.fullName", "tipoEncuadernacion", "tipoCubierta", "tipoImpresion",
|
||||
@ -74,6 +88,7 @@ public class PresupuestoDatatableService {
|
||||
.add("updatedAt", p -> formatDate(p.getUpdatedAt(), locale))
|
||||
.addIf(!publico, "user", p -> p.getUser() != null ? p.getUser().getFullName() : "")
|
||||
.add("actions", this::generarBotones)
|
||||
.where(base)
|
||||
.toJson(count);
|
||||
}
|
||||
|
||||
|
||||
@ -920,4 +920,28 @@ public class Presupuesto extends AbstractAuditedEntity implements Cloneable {
|
||||
public void setId(Long 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;
|
||||
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
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.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.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.imprimelibros.erp.direcciones.Direccion;
|
||||
|
||||
@Service
|
||||
public class UserServiceImpl implements UserService {
|
||||
|
||||
@ -29,5 +40,4 @@ public class UserServiceImpl implements UserService {
|
||||
if (query == null || query.isBlank()) query = null;
|
||||
return userDao.searchUsers(role, query, pageable);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
spring.application.name=erp
|
||||
|
||||
server.forward-headers-strategy=framework
|
||||
server.servlet.session.cookie.secure=true
|
||||
|
||||
|
||||
#
|
||||
# Logging
|
||||
#
|
||||
@ -11,8 +15,8 @@ logging.level.org.springframework=ERROR
|
||||
#
|
||||
# Database Configuration
|
||||
#
|
||||
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://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.username=imprimelibros_user
|
||||
spring.datasource.password=om91irrDctd
|
||||
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.user=tu_user
|
||||
# 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:
|
||||
file: db/changelog/changesets/0003-create-paises.yml
|
||||
- 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.aceptar=Aceptar
|
||||
app.cancelar=Cancelar
|
||||
app.seleccionar=Seleccionar
|
||||
app.guardar=Guardar
|
||||
app.editar=Editar
|
||||
app.add=Añadir
|
||||
|
||||
@ -4,11 +4,47 @@ cart.empty=Tu cesta de la compra está vacía.
|
||||
cart.item.presupuesto-numero=Presupuesto #
|
||||
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.base=Base imponible:
|
||||
cart.resumen.iva-4=IVA 4%:
|
||||
cart.resumen.iva-21=IVA 21%:
|
||||
cart.resumen.total=Total cesta:
|
||||
cart.resumen.base=Base imponible
|
||||
cart.resumen.envio=Coste de envío
|
||||
cart.resumen.iva-4=IVA 4%
|
||||
cart.resumen.iva-21=IVA 21%
|
||||
cart.resumen.descuento=Descuento fidelización
|
||||
cart.resumen.total=Total cesta
|
||||
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.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.
|
||||
|
||||
|
||||
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;
|
||||
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 { showLoader, hideLoader } from '../loader.js';
|
||||
|
||||
$(() => {
|
||||
|
||||
updateTotal();
|
||||
$(document).ajaxStart(showLoader).ajaxStop(hideLoader);
|
||||
|
||||
function updateTotal() {
|
||||
const items = $(".product");
|
||||
let iva4 = 0;
|
||||
let iva21 = 0;
|
||||
let base = 0;
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = $(items[i]);
|
||||
const b = item.data("base");
|
||||
const i4 = item.data("iva-4");
|
||||
const i21 = item.data("iva-21");
|
||||
base += parseFloat(b) || 0;
|
||||
iva4 += parseFloat(i4) || 0;
|
||||
iva21 += parseFloat(i21) || 0;
|
||||
$(document).on('updateCart', () => {
|
||||
// get form and submit
|
||||
const form = $('#cartForm');
|
||||
const container = $("#onlyOneShipment").is(':checked') ? $('#shippingAddressesContainer') : $('.product');
|
||||
// remove name from container . direccion-card
|
||||
container.find('.direccion-card input[type="hidden"]').removeAttr('name');
|
||||
|
||||
container.find('.direccion-card').each(function (i) {
|
||||
$(this).find('.direccion-id').attr('name', 'direcciones[' + i + '].id');
|
||||
$(this).find('.direccion-cp').attr('name', 'direcciones[' + i + '].cp');
|
||||
$(this).find('.direccion-pais-code3').attr('name', 'direcciones[' + i + '].paisCode3');
|
||||
if ($(this).find('.presupuesto-id').length > 0 && $(this).find('.presupuesto-id').val() !== null
|
||||
&& $(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));
|
||||
if (iva4 > 0) {
|
||||
$("#iva-4-cesta").text(formateaMoneda(iva4));
|
||||
$("#tr-iva-4").show();
|
||||
} else {
|
||||
$("#tr-iva-4").hide();
|
||||
else {
|
||||
$('.cart-content').removeClass('d-none');
|
||||
$("#alert-empty").addClass("d-none");
|
||||
// check if select2 is initialized
|
||||
if ($('#select-customer').length && !$('#select-customer').hasClass('select2-hidden-accessible')) {
|
||||
initMoveCartToCustomer();
|
||||
}
|
||||
}
|
||||
if (iva21 > 0) {
|
||||
$("#iva-21-cesta").text(formateaMoneda(iva21));
|
||||
$("#tr-iva-21").show();
|
||||
} else {
|
||||
$("#tr-iva-21").hide();
|
||||
if ($('#onlyOneShipment').is(':checked')) {
|
||||
if ($("#shippingAddressesContainer .direccion-card").length === 0) {
|
||||
$(".alert-shipment").removeClass("d-none");
|
||||
$('#btn-checkout').prop('disabled', true);
|
||||
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) {
|
||||
|
||||
event.preventDefault();
|
||||
const cartItemId = $(this).data("cart-item-id");
|
||||
const presupuestoId = $(this).data("cart-item-id");
|
||||
const card = $(this).closest('.card.product');
|
||||
|
||||
// CSRF (Spring Security)
|
||||
@ -46,7 +113,7 @@ $(() => {
|
||||
const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.content || 'X-CSRF-TOKEN';
|
||||
|
||||
try {
|
||||
const res = await fetch(`/cart/delete/item/${cartItemId}`, {
|
||||
const res = await fetch(`/cart/delete/item/${presupuestoId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { [csrfHeader]: csrfToken }
|
||||
});
|
||||
@ -55,13 +122,81 @@ $(() => {
|
||||
console.error('Error al eliminar. Status:', res.status);
|
||||
return;
|
||||
}
|
||||
else{
|
||||
else {
|
||||
card?.remove();
|
||||
updateTotal();
|
||||
$(document).trigger('updateCart');
|
||||
}
|
||||
|
||||
} catch (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 -->
|
||||
<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-base=${item.base}">
|
||||
<div class="card-body">
|
||||
<div class="row gy-3">
|
||||
|
||||
<div class="col-sm-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">
|
||||
<input type="hidden" class="item-presupuesto-id" th:value="${item.presupuestoId}" />
|
||||
<input type="hidden" class="item-tirada" th:value="${item.tirada}" />
|
||||
|
||||
<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>
|
||||
|
||||
<!-- Detalles -->
|
||||
<div class="col-sm">
|
||||
<!-- Título / enlace -->
|
||||
<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>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show tab-pane-shipping" th:id="${'pills-shipping-' + item.cartItemId}"
|
||||
role="tabpanel" th:aria-labelledby="${'pills-shipping-' + item.cartItemId + '-tab'}">
|
||||
|
||||
<!-- Detalles opcionales (ej: cliente, fecha, etc.) -->
|
||||
<ul class="list-inline text-muted mb-1">
|
||||
<div th:each="linea : ${item.resumen.lineas}">
|
||||
<li class="list-inline-item me-3">
|
||||
<div th:utext="${linea['descripcion']}"></div>
|
||||
</li>
|
||||
<div class="col-sm">
|
||||
<!-- Título / enlace -->
|
||||
<h5 class="fs-18 text-truncate my-1 p-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>
|
||||
<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>
|
||||
</ul>
|
||||
|
||||
<ul class="list-inline text-muted mb-1" th:if="${item.resumen.servicios != null}">
|
||||
<span th:utext="#{pdf.servicios-adicionales}">Servicios adicionales</span>
|
||||
<span class="spec-label" th:text="${item.resumen.servicios}"></span>
|
||||
</ul>
|
||||
<div class="card ribbon-box border shadow-none mb-lg-0 material-shadow mt-4"
|
||||
th:if="${item.hasSample}">
|
||||
<div class="card-body">
|
||||
<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
|
||||
and #maps.containsKey(item.resumen, 'datosMaquetacion')
|
||||
and item.resumen['datosMaquetacion'] != null}">
|
||||
<li class="list-inline-item spec-row mb-1">
|
||||
<span th:text="#{pdf.datos-maquetacion}">Datos de maquetación:</span>
|
||||
<span th:utext="${item.resumen.datosMaquetacion}"></span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="ribbon-content mt-4">
|
||||
<div class="px-2 mb-2">
|
||||
<button type="button" class="btn btn-secondary btn-add-shipping-sample"
|
||||
th:text="#{cart.shipping.add}">Añadir dirección</button>
|
||||
|
||||
<ul class="list-inline text-muted mb-1" th:if="${item.resumen != null
|
||||
and #maps.containsKey(item.resumen, 'datosMarcapaginas')
|
||||
and item.resumen['datosMarcapaginas'] != null}">
|
||||
<li class="list-inline-item 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 class="shipping-addresses-sample 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.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>
|
||||
|
||||
<!-- Precio o totales (si los tienes) -->
|
||||
<div class="col-sm-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>
|
||||
</th:block>
|
||||
</th:block>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-auto div-shipping-product">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -77,7 +198,7 @@
|
||||
<!-- Botón eliminar -->
|
||||
<div>
|
||||
<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
|
||||
</a>
|
||||
</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" />
|
||||
</th:block>
|
||||
<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>
|
||||
</head>
|
||||
|
||||
@ -22,6 +22,10 @@
|
||||
<th:block layout:fragment="content">
|
||||
<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">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
@ -32,67 +36,12 @@
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="container-fluid row gy-4">
|
||||
<div th:if="${items.isEmpty()}">
|
||||
<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 th:if="${items.isEmpty()}">
|
||||
<div id="alert-empty"class="alert alert-info" role="alert" th:text="#{cart.empty}"></div>
|
||||
</div>
|
||||
|
||||
<div th:insert="~{imprimelibros/cart/_cartContent :: cartContent(${items}, ${cartId})}"></div>
|
||||
|
||||
</th:block>
|
||||
|
||||
<th:block th:replace="~{theme/partials/vendor-scripts :: scripts}" />
|
||||
@ -102,6 +51,7 @@
|
||||
</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>
|
||||
</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">
|
||||
<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'">
|
||||
|
||||
@ -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>
|
||||
<!--page title-->
|
||||
<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 -->
|
||||
<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