diff --git a/src/main/java/com/imprimelibros/erp/cart/Cart.java b/src/main/java/com/imprimelibros/erp/cart/Cart.java new file mode 100644 index 0000000..ff3b6ec --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/cart/Cart.java @@ -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 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 getItems() { return items; } + public void setItems(List items) { this.items = items; } +} diff --git a/src/main/java/com/imprimelibros/erp/cart/CartController.java b/src/main/java/com/imprimelibros/erp/cart/CartController.java new file mode 100644 index 0000000..1741ddd --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/cart/CartController.java @@ -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"; + } +} diff --git a/src/main/java/com/imprimelibros/erp/cart/CartItem.java b/src/main/java/com/imprimelibros/erp/cart/CartItem.java new file mode 100644 index 0000000..853214c --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/cart/CartItem.java @@ -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; } +} + diff --git a/src/main/java/com/imprimelibros/erp/cart/CartItemRepository.java b/src/main/java/com/imprimelibros/erp/cart/CartItemRepository.java new file mode 100644 index 0000000..28035c5 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/cart/CartItemRepository.java @@ -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 { + + List findByCartId(Long cartId); + + Optional findByCartIdAndPresupuestoId(Long cartId, Long presupuestoId); + + boolean existsByCartIdAndPresupuestoId(Long cartId, Long presupuestoId); + + long deleteByCartId(Long cartId); +} diff --git a/src/main/java/com/imprimelibros/erp/cart/CartRepository.java b/src/main/java/com/imprimelibros/erp/cart/CartRepository.java new file mode 100644 index 0000000..d23d60a --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/cart/CartRepository.java @@ -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 { + Optional findByUserIdAndStatus(Long userId, Cart.Status status); +} diff --git a/src/main/java/com/imprimelibros/erp/cart/CartService.java b/src/main/java/com/imprimelibros/erp/cart/CartService.java new file mode 100644 index 0000000..155f8f4 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/cart/CartService.java @@ -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 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(); + } +} diff --git a/src/main/java/com/imprimelibros/erp/presupuesto/validation/PaginasCosido.java b/src/main/java/com/imprimelibros/erp/presupuesto/validation/PaginasCosido.java new file mode 100644 index 0000000..68eb797 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/presupuesto/validation/PaginasCosido.java @@ -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[] payload() default {}; +} diff --git a/src/main/java/com/imprimelibros/erp/presupuesto/validation/PaginasCosidoValidator.java b/src/main/java/com/imprimelibros/erp/presupuesto/validation/PaginasCosidoValidator.java new file mode 100644 index 0000000..c86a422 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/presupuesto/validation/PaginasCosidoValidator.java @@ -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 { + + @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; + } +} diff --git a/src/main/resources/static/assets/js/pages/imprimelibros/cart-badge.js b/src/main/resources/static/assets/js/pages/imprimelibros/cart-badge.js new file mode 100644 index 0000000..1852ffd --- /dev/null +++ b/src/main/resources/static/assets/js/pages/imprimelibros/cart-badge.js @@ -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); +}); \ No newline at end of file diff --git a/src/main/resources/templates/imprimelibros/layout.html b/src/main/resources/templates/imprimelibros/layout.html index 49dc1dc..d6370b9 100644 --- a/src/main/resources/templates/imprimelibros/layout.html +++ b/src/main/resources/templates/imprimelibros/layout.html @@ -39,6 +39,10 @@ + + + + \ No newline at end of file diff --git a/src/main/resources/templates/imprimelibros/partials/topbar.html b/src/main/resources/templates/imprimelibros/partials/topbar.html index 703ef04..1310ac6 100644 --- a/src/main/resources/templates/imprimelibros/partials/topbar.html +++ b/src/main/resources/templates/imprimelibros/partials/topbar.html @@ -54,8 +54,8 @@ - + user-image English @@ -64,6 +64,19 @@ +
+ +
+ +