estructura inicial de carrito hecha

This commit is contained in:
2025-10-14 15:16:02 +02:00
parent 90376e61c8
commit a33ba3256b
11 changed files with 408 additions and 3 deletions

View File

@ -0,0 +1,56 @@
package com.imprimelibros.erp.cart;
import jakarta.persistence.*;
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"}))
public class Cart {
public enum Status { ACTIVE, LOCKED, ABANDONED }
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "user_id", nullable = false)
private Long userId;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 16)
private Status status = Status.ACTIVE;
@Column(nullable = false, length = 3)
private String currency = "EUR";
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt = LocalDateTime.now();
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt = LocalDateTime.now();
@OneToMany(mappedBy = "cart", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<CartItem> items = new ArrayList<>();
@PreUpdate
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 Status getStatus() { return status; }
public void setStatus(Status status) { this.status = status; }
public String getCurrency() { return currency; }
public void setCurrency(String currency) { this.currency = currency; }
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; }
}

View File

@ -0,0 +1,97 @@
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 com.imprimelibros.erp.users.User;
import org.springframework.security.core.Authentication;
import java.security.Principal;
@Controller
@RequestMapping("/cart")
public class CartController {
private final CartService service;
public CartController(CartService service) {
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");
}
/** Vista del carrito */
@GetMapping
public String viewCart(Model model, Principal principal) {
var items = service.listItems(currentUserId(principal));
model.addAttribute("items", items);
return "imprimelibros/cart/cart"; // crea esta vista si quieres (tabla simple)
}
/** Añadir presupuesto via POST form */
@PostMapping("/add")
public String add(@RequestParam("presupuestoId") Long presupuestoId, Principal principal) {
service.addPresupuesto(currentUserId(principal), presupuestoId);
return "redirect:/cart";
}
/** Añadir presupuesto con ruta REST (opcional) */
@PostMapping("/add/{presupuestoId}")
public String addPath(@PathVariable Long presupuestoId, Principal principal) {
service.addPresupuesto(currentUserId(principal), presupuestoId);
return "redirect:/cart";
}
@GetMapping("/count")
@ResponseBody
public long getCount(Principal principal) {
if (principal == null)
return 0;
return service.countItems(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);
return "redirect:/cart";
}
/** Eliminar línea por presupuesto_id (opcional) */
@DeleteMapping("/remove/presupuesto/{presupuestoId}")
public String removeByPresupuesto(@PathVariable Long presupuestoId, Principal principal) {
service.removeByPresupuesto(currentUserId(principal), presupuestoId);
return "redirect:/cart";
}
/** Vaciar carrito completo */
@DeleteMapping("/clear")
public String clear(Principal principal) {
service.clear(currentUserId(principal));
return "redirect:/cart";
}
}

View File

@ -0,0 +1,37 @@
package com.imprimelibros.erp.cart;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(
name = "cart_items",
uniqueConstraints = @UniqueConstraint(name="uq_cartitem_unique", columnNames={"cart_id","presupuesto_id"})
)
public class CartItem {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "cart_id", nullable = false)
private Cart cart;
@Column(name = "presupuesto_id", nullable = false)
private Long presupuestoId;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt = LocalDateTime.now();
// Getters & Setters
public Long getId() { return id; }
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 LocalDateTime getCreatedAt() { return createdAt; }
}

View File

@ -0,0 +1,17 @@
package com.imprimelibros.erp.cart;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.Optional;
public interface CartItemRepository extends JpaRepository<CartItem, Long> {
List<CartItem> findByCartId(Long cartId);
Optional<CartItem> findByCartIdAndPresupuestoId(Long cartId, Long presupuestoId);
boolean existsByCartIdAndPresupuestoId(Long cartId, Long presupuestoId);
long deleteByCartId(Long cartId);
}

View File

@ -0,0 +1,9 @@
package com.imprimelibros.erp.cart;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface CartRepository extends JpaRepository<Cart, Long> {
Optional<Cart> findByUserIdAndStatus(Long userId, Cart.Status status);
}

View File

@ -0,0 +1,89 @@
package com.imprimelibros.erp.cart;
import jakarta.transaction.Transactional;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class CartService {
private final CartRepository cartRepo;
private final CartItemRepository itemRepo;
public CartService(CartRepository cartRepo, CartItemRepository itemRepo) {
this.cartRepo = cartRepo;
this.itemRepo = itemRepo;
}
/** Devuelve el carrito activo o lo crea si no existe. */
@Transactional
public Cart getOrCreateActiveCart(Long userId) {
return cartRepo.findByUserIdAndStatus(userId, Cart.Status.ACTIVE)
.orElseGet(() -> {
Cart c = new Cart();
c.setUserId(userId);
c.setStatus(Cart.Status.ACTIVE);
return cartRepo.save(c);
});
}
/** Lista items (presupuestos) del carrito activo del usuario. */
@Transactional
public List<CartItem> listItems(Long userId) {
Cart cart = getOrCreateActiveCart(userId);
return itemRepo.findByCartId(cart.getId());
}
/** Añade un presupuesto al carrito. Si ya está, no hace nada (idempotente). */
@Transactional
public void addPresupuesto(Long userId, Long presupuestoId) {
Cart cart = getOrCreateActiveCart(userId);
boolean exists = itemRepo.existsByCartIdAndPresupuestoId(cart.getId(), presupuestoId);
if (!exists) {
CartItem ci = new CartItem();
ci.setCart(cart);
ci.setPresupuestoId(presupuestoId);
itemRepo.save(ci);
}
}
/** Elimina una línea del carrito por ID de item (validando pertenencia). */
@Transactional
public void removeItem(Long userId, Long itemId) {
Cart cart = getOrCreateActiveCart(userId);
CartItem item = itemRepo.findById(itemId).orElseThrow(() -> new IllegalArgumentException("Item no existe"));
if (!item.getCart().getId().equals(cart.getId()))
throw new IllegalStateException("El item no pertenece a tu carrito");
itemRepo.delete(item);
}
/** Elimina una línea del carrito buscando por presupuesto_id. */
@Transactional
public void removeByPresupuesto(Long userId, Long presupuestoId) {
Cart cart = getOrCreateActiveCart(userId);
itemRepo.findByCartIdAndPresupuestoId(cart.getId(), presupuestoId)
.ifPresent(itemRepo::delete);
}
/** Vacía todo el carrito activo. */
@Transactional
public void clear(Long userId) {
Cart cart = getOrCreateActiveCart(userId);
itemRepo.deleteByCartId(cart.getId());
}
/** Marca el carrito como bloqueado (por ejemplo, antes de crear un pedido). */
@Transactional
public void lockCart(Long userId) {
Cart cart = getOrCreateActiveCart(userId);
cart.setStatus(Cart.Status.LOCKED);
cartRepo.save(cart);
}
@Transactional
public long countItems(Long userId) {
Cart cart = getOrCreateActiveCart(userId);
return itemRepo.findByCartId(cart.getId()).size();
}
}

View File

@ -0,0 +1,16 @@
package com.imprimelibros.erp.presupuesto.validation;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
@Documented
@Constraint(validatedBy = PaginasCosidoValidator.class)
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PaginasCosido {
String message() default "Las tiradas deben ser todas mayores o todas menores al valor POD";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@ -0,0 +1,38 @@
package com.imprimelibros.erp.presupuesto.validation;
import com.imprimelibros.erp.configurationERP.VariableService;
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
public class PaginasCosidoValidator implements ConstraintValidator<PaginasCosido, Presupuesto> {
@Autowired
private MessageSource messageSource;
@Override
public boolean isValid(Presupuesto presupuesto, ConstraintValidatorContext context) {
if (presupuesto == null)
return true;
if (presupuesto.getTipoEncuadernacion() != null &&
presupuesto.getTipoEncuadernacion() == Presupuesto.TipoEncuadernacion.cosido) {
if (presupuesto.getPaginasColor() > 0 && presupuesto.getPaginasNegro() > 0) {
String mensajeInterpolado = messageSource.getMessage(
"presupuesto.errores.tipo-paginas-cosido",
null,
LocaleContextHolder.getLocale() // respeta el idioma actual
);
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(mensajeInterpolado)
.addConstraintViolation();
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,28 @@
$(() => {
const badge = document.getElementById("cart-item-count");
if (!badge) return;
function updateCartCount() {
fetch("/cart/count")
.then(res => res.ok ? res.text() : "0")
.then(count => {
const n = parseInt(count || "0", 10);
if (isNaN(n) || n === 0) {
badge.classList.add("d-none");
} else {
badge.textContent = n;
badge.classList.remove("d-none");
}
})
.catch(() => badge.classList.add("d-none"));
}
// Actualizar al cargar
updateCartCount();
// Si quieres refrescar cada 60s:
setInterval(updateCartCount, 60000);
// generate a custom event to update the cart count from other scripts
document.addEventListener("update-cart", updateCartCount);
});

View File

@ -39,6 +39,10 @@
<th:block layout:fragment="pagejs" />
<script th:src="@{/assets/js/app.js}"></script>
<script th:src="@{/assets/js/pages/imprimelibros/languageBundle.js}"></script>
<th:block th:if="${#authorization.expression('isAuthenticated()')}">
<script src="/assets/js/pages/imprimelibros/cart-badge.js"></script>
</th:block>
</body>
</html>

View File

@ -54,8 +54,8 @@
</a>
<!-- item-->
<a href="javascript:void(0);" class="dropdown-item notify-item language py-2" data-lang="en-GB"
title="English">
<a href="javascript:void(0);" class="dropdown-item notify-item language py-2"
data-lang="en-GB" title="English">
<img src="/assets/images/flags/gb.svg" alt="user-image" class="me-2 rounded"
height="18">
<span class="align-middle">English</span>
@ -64,6 +64,19 @@
</div>
</div>
<div th:if="${#authorization.expression('isAuthenticated()')}"
class="ms-1 header-item d-none d-sm-flex">
<button type="button" id="btn_cart"
class="btn btn-icon btn-topbar material-shadow-none btn-ghost-secondary rounded-circle light-dark-mode">
<i class="bx bx-cart fs-22"></i>
<span id="cart-item-count"
class="position-absolute topbar-badge cartitem-badge fs-10 translate-middle badge rounded-pill bg-info d-none">
0
</span>
</button>
</div>
<div th:if="${#authorization.expression('isAuthenticated()')}">
<div class="dropdown ms-sm-3 header-item topbar-user">
@ -92,7 +105,8 @@
<a class="dropdown-item" href="#"
onclick="document.getElementById('logoutForm').submit(); return false;">
<i class="mdi mdi-logout text-muted fs-16 align-middle me-1"></i>
<span class="align-middle" data-key="t-logout" th:text="#{app.logout}">Cerrar sesión</span>
<span class="align-middle" data-key="t-logout" th:text="#{app.logout}">Cerrar
sesión</span>
</a>
</div>
</div>