mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-01-31 08:08:50 +00:00
Compare commits
33 Commits
892c473266
...
1888850a64
| Author | SHA1 | Date | |
|---|---|---|---|
| 1888850a64 | |||
| 06e03afa04 | |||
| 46715d1017 | |||
| ea8a005cde | |||
| ff9c04afb6 | |||
| 060b435388 | |||
| f26f96a490 | |||
| f20dd9068a | |||
| 70856edc12 | |||
| 9f33db4055 | |||
| a33ba3256b | |||
| 90376e61c8 | |||
| 37ae61d6f7 | |||
| 9b0a79e2cd | |||
| 47344c94a9 | |||
| 543ff9a079 | |||
| d99ef65268 | |||
| 9d88392a2b | |||
| 9ebe2a3419 | |||
| c15fff73ee | |||
| 99d27cd3ed | |||
| 26c2ca543a | |||
| 6641c1f077 | |||
| 62dcff8869 | |||
| a1359f37b0 | |||
| d4d83fe118 | |||
| 6c4b63daa6 | |||
| 328ff509e3 | |||
| 2b53579a48 | |||
| 389ac22b68 | |||
| 1e8f9cafb3 | |||
| b2f3ef042e | |||
| 14ca264ae2 |
33
pom.xml
33
pom.xml
@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.5.3</version>
|
||||
<version>3.5.6</version>
|
||||
<relativePath /> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>com.imprimelibros</groupId>
|
||||
@ -120,6 +120,37 @@
|
||||
<version>8.10.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- GeoIP2 (MaxMind) -->
|
||||
<dependency>
|
||||
<groupId>com.maxmind.geoip2</groupId>
|
||||
<artifactId>geoip2</artifactId>
|
||||
<version>4.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- HTTP client (Spring Web) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- PDF generation -->
|
||||
<dependency>
|
||||
<groupId>com.openhtmltopdf</groupId>
|
||||
<artifactId>openhtmltopdf-pdfbox</artifactId>
|
||||
<version>1.0.10</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.openhtmltopdf</groupId>
|
||||
<artifactId>openhtmltopdf-slf4j</artifactId>
|
||||
<version>1.0.10</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@ -2,8 +2,10 @@ package com.imprimelibros.erp;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||
|
||||
@SpringBootApplication
|
||||
@ConfigurationPropertiesScan(basePackages = "com.imprimelibros.erp")
|
||||
public class ErpApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
@ -6,15 +6,10 @@ import java.security.SecureRandom;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Base64;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.thymeleaf.context.Context;
|
||||
import org.thymeleaf.spring6.SpringTemplateEngine;
|
||||
|
||||
import com.imprimelibros.erp.common.email.EmailService;
|
||||
import com.imprimelibros.erp.users.User;
|
||||
@ -26,26 +21,17 @@ public class PasswordResetService {
|
||||
private final PasswordResetTokenRepository tokenRepo;
|
||||
private final UserDao userRepo;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final JavaMailSender mailSender;
|
||||
private final SpringTemplateEngine templateEngine;
|
||||
private final MessageSource messages;
|
||||
private final EmailService emailService;
|
||||
|
||||
public PasswordResetService(
|
||||
PasswordResetTokenRepository tokenRepo,
|
||||
UserDao userRepo,
|
||||
PasswordEncoder passwordEncoder,
|
||||
JavaMailSender mailSender,
|
||||
SpringTemplateEngine templateEngine,
|
||||
MessageSource messages,
|
||||
EmailService emailService
|
||||
) {
|
||||
this.tokenRepo = tokenRepo;
|
||||
this.userRepo = userRepo;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.mailSender = mailSender;
|
||||
this.templateEngine = templateEngine;
|
||||
this.messages = messages;
|
||||
this.emailService = emailService;
|
||||
}
|
||||
|
||||
|
||||
56
src/main/java/com/imprimelibros/erp/cart/Cart.java
Normal file
56
src/main/java/com/imprimelibros/erp/cart/Cart.java
Normal 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; }
|
||||
}
|
||||
110
src/main/java/com/imprimelibros/erp/cart/CartController.java
Normal file
110
src/main/java/com/imprimelibros/erp/cart/CartController.java
Normal file
@ -0,0 +1,110 @@
|
||||
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 jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import com.imprimelibros.erp.users.User;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
@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, Locale locale) {
|
||||
var items = service.listItems(currentUserId(principal), locale);
|
||||
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(@PathVariable(name = "presupuestoId", required = true) Long presupuestoId, Principal principal) {
|
||||
service.addPresupuesto(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);
|
||||
boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
|
||||
if (isAjax) {
|
||||
// Responder 200 con la URL a la que quieres ir
|
||||
return ResponseEntity.ok(
|
||||
Map.of("redirect", "/cart"));
|
||||
}
|
||||
// Navegación normal: redirección server-side
|
||||
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("/delete/item/{presupuestoId}")
|
||||
@ResponseBody
|
||||
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";
|
||||
}
|
||||
}
|
||||
37
src/main/java/com/imprimelibros/erp/cart/CartItem.java
Normal file
37
src/main/java/com/imprimelibros/erp/cart/CartItem.java
Normal 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; }
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
154
src/main/java/com/imprimelibros/erp/cart/CartService.java
Normal file
154
src/main/java/com/imprimelibros/erp/cart/CartService.java
Normal file
@ -0,0 +1,154 @@
|
||||
package com.imprimelibros.erp.cart;
|
||||
|
||||
import jakarta.transaction.Transactional;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.imprimelibros.erp.presupuesto.classes.PresupuestoFormatter;
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||
import com.imprimelibros.erp.common.Utils;
|
||||
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;
|
||||
|
||||
public CartService(CartRepository cartRepo, CartItemRepository itemRepo,
|
||||
MessageSource messageSource, PresupuestoFormatter presupuestoFormatter,
|
||||
PresupuestoRepository presupuestoRepo, Utils utils) {
|
||||
this.cartRepo = cartRepo;
|
||||
this.itemRepo = itemRepo;
|
||||
this.messageSource = messageSource;
|
||||
this.presupuestoFormatter = presupuestoFormatter;
|
||||
this.presupuestoRepo = presupuestoRepo;
|
||||
this.utils = utils;
|
||||
}
|
||||
|
||||
/** 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<Map<String, Object>> listItems(Long userId, Locale locale) {
|
||||
Cart cart = getOrCreateActiveCart(userId);
|
||||
List<Map<String, Object>> resultados = new ArrayList<>();
|
||||
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()));
|
||||
|
||||
Map<String, Object> elemento = getElementoCart(p, locale);
|
||||
elemento.put("cartItemId", item.getId());
|
||||
resultados.add(elemento);
|
||||
}
|
||||
//System.out.println("Cart items: " + resultados);
|
||||
return resultados;
|
||||
}
|
||||
|
||||
/** 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();
|
||||
}
|
||||
|
||||
private Map<String, Object> getElementoCart(Presupuesto presupuesto, Locale locale) {
|
||||
|
||||
Map<String, Object> resumen = new HashMap<>();
|
||||
|
||||
resumen.put("titulo", presupuesto.getTitulo());
|
||||
|
||||
resumen.put("imagen",
|
||||
"/assets/images/imprimelibros/presupuestador/" + presupuesto.getTipoEncuadernacion() + ".png");
|
||||
resumen.put("imagen_alt",
|
||||
messageSource.getMessage("presupuesto." + presupuesto.getTipoEncuadernacion(), null, locale));
|
||||
|
||||
resumen.put("presupuestoId", presupuesto.getId());
|
||||
|
||||
Map<String, Object> detalles = utils.getTextoPresupuesto(presupuesto, locale);
|
||||
|
||||
resumen.put("baseTotal", Utils.formatCurrency(presupuesto.getBaseImponible(), locale));
|
||||
resumen.put("base", presupuesto.getBaseImponible());
|
||||
resumen.put("iva4", presupuesto.getIvaImporte4());
|
||||
resumen.put("iva21", presupuesto.getIvaImporte21());
|
||||
|
||||
resumen.put("resumen", detalles);
|
||||
|
||||
return resumen;
|
||||
}
|
||||
}
|
||||
237
src/main/java/com/imprimelibros/erp/common/Utils.java
Normal file
237
src/main/java/com/imprimelibros/erp/common/Utils.java
Normal file
@ -0,0 +1,237 @@
|
||||
package com.imprimelibros.erp.common;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
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;
|
||||
|
||||
@Component
|
||||
public class Utils {
|
||||
|
||||
private final PresupuestoFormatter presupuestoFormatter;
|
||||
private final MessageSource messageSource;
|
||||
|
||||
public Utils(PresupuestoFormatter presupuestoFormatter,
|
||||
MessageSource messageSource) {
|
||||
this.presupuestoFormatter = presupuestoFormatter;
|
||||
this.messageSource = messageSource;
|
||||
}
|
||||
|
||||
public static String formatCurrency(BigDecimal amount, Locale locale) {
|
||||
NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance(locale);
|
||||
return currencyFormatter.format(amount);
|
||||
}
|
||||
|
||||
public static String formatCurrency(Double amount, Locale locale) {
|
||||
NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance(locale);
|
||||
return currencyFormatter.format(amount);
|
||||
}
|
||||
|
||||
public Map<String, Object> getTextoPresupuesto(Presupuesto presupuesto, Locale locale) {
|
||||
|
||||
Map<String, Object> resumen = new HashMap<>();
|
||||
|
||||
resumen.put("titulo", presupuesto.getTitulo());
|
||||
|
||||
resumen.put("imagen",
|
||||
"/assets/images/imprimelibros/presupuestador/" + presupuesto.getTipoEncuadernacion()
|
||||
+ ".png");
|
||||
resumen.put("imagen_alt",
|
||||
messageSource.getMessage("presupuesto." + presupuesto.getTipoEncuadernacion(), null,
|
||||
locale));
|
||||
|
||||
resumen.put("presupuestoId", presupuesto.getId());
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
List<Map<String, Object>> servicios = new ArrayList<>();
|
||||
if (presupuesto.getServiciosJson() != null && !presupuesto.getServiciosJson().isBlank())
|
||||
try {
|
||||
servicios = mapper.readValue(presupuesto.getServiciosJson(), new TypeReference<>() {
|
||||
});
|
||||
} catch (JsonProcessingException e) {
|
||||
// Manejar la excepción
|
||||
}
|
||||
|
||||
boolean hayDepositoLegal = servicios != null && servicios.stream()
|
||||
.map(m -> java.util.Objects.toString(m.get("id"), ""))
|
||||
.map(String::trim)
|
||||
.anyMatch("deposito-legal"::equals);
|
||||
|
||||
List<HashMap<String, Object>> lineas = new ArrayList<>();
|
||||
HashMap<String, Object> linea = new HashMap<>();
|
||||
Double precio_unitario = 0.0;
|
||||
Double precio_total = 0.0;
|
||||
BigDecimal total = BigDecimal.ZERO;
|
||||
linea.put("descripcion", presupuestoFormatter.resumen(presupuesto, servicios, locale));
|
||||
linea.put("cantidad", presupuesto.getSelectedTirada() != null ? presupuesto.getSelectedTirada() : 0);
|
||||
precio_unitario = (presupuesto.getPrecioUnitario() != null
|
||||
? presupuesto.getPrecioUnitario().doubleValue()
|
||||
: 0.0);
|
||||
precio_total = (presupuesto.getPrecioTotalTirada() != null
|
||||
? presupuesto.getPrecioTotalTirada().doubleValue()
|
||||
: 0.0);
|
||||
linea.put("precio_unitario", precio_unitario);
|
||||
linea.put("precio_total", BigDecimal.valueOf(precio_total).setScale(2, RoundingMode.HALF_UP));
|
||||
total = total.add(BigDecimal.valueOf(precio_total));
|
||||
lineas.add(linea);
|
||||
|
||||
if (hayDepositoLegal) {
|
||||
linea = new HashMap<>();
|
||||
linea.put("descripcion",
|
||||
messageSource.getMessage("pdf.ejemplares-deposito-legal", new Object[] { 4 },
|
||||
locale));
|
||||
lineas.add(linea);
|
||||
}
|
||||
|
||||
String serviciosExtras = "";
|
||||
if (servicios != null) {
|
||||
for (Map<String, Object> servicio : servicios) {
|
||||
if ("deposito-legal".equals(servicio.get("id")) ||
|
||||
"service-isbn".equals(servicio.get("id"))) {
|
||||
serviciosExtras += messageSource.getMessage(
|
||||
"presupuesto.extras-" + servicio.get("id"), null, locale)
|
||||
+ ", ";
|
||||
} else {
|
||||
serviciosExtras += messageSource.getMessage(
|
||||
"presupuesto.extras-" + servicio.get("id"), null, locale)
|
||||
.toLowerCase() + ", ";
|
||||
}
|
||||
}
|
||||
if (!serviciosExtras.isEmpty()) {
|
||||
serviciosExtras = serviciosExtras.substring(0, serviciosExtras.length() - 2);
|
||||
;
|
||||
}
|
||||
if (servicios.stream().anyMatch(service -> "marcapaginas".equals(service.get("id")))) {
|
||||
ObjectMapper mapperServicio = new ObjectMapper();
|
||||
Object raw = presupuesto.getDatosMarcapaginasJson();
|
||||
Map<String, Object> datosMarcapaginas;
|
||||
String descripcion = "";
|
||||
try {
|
||||
if (raw instanceof String s) {
|
||||
datosMarcapaginas = mapperServicio.readValue(s,
|
||||
new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
} else if (raw instanceof Map<?, ?> m) {
|
||||
datosMarcapaginas = mapperServicio.convertValue(m,
|
||||
new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
"Tipo no soportado para datosMarcapaginas: "
|
||||
+ raw);
|
||||
}
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException("Error parsing datosMarcapaginasJson", e);
|
||||
}
|
||||
descripcion += "<br/><ul><li>";
|
||||
descripcion += Marcapaginas.Tamanios
|
||||
.valueOf(datosMarcapaginas.get("tamanio").toString()).getLabel()
|
||||
+ ", ";
|
||||
descripcion += Marcapaginas.Caras_Impresion
|
||||
.valueOf(datosMarcapaginas.get("carasImpresion").toString())
|
||||
.getMessageKey() + ", ";
|
||||
descripcion += messageSource
|
||||
.getMessage(Marcapaginas.Papeles
|
||||
.valueOf(datosMarcapaginas.get("papel")
|
||||
.toString())
|
||||
.getMessageKey(), null, locale)
|
||||
+ " - " +
|
||||
datosMarcapaginas.get("gramaje").toString() + " gr, ";
|
||||
descripcion += messageSource.getMessage(
|
||||
Marcapaginas.Acabado.valueOf(
|
||||
datosMarcapaginas.get("acabado").toString())
|
||||
.getMessageKey(),
|
||||
null, locale);
|
||||
descripcion += "</li></ul>";
|
||||
resumen.put("datosMarcapaginas", descripcion);
|
||||
}
|
||||
if (servicios.stream().anyMatch(service -> "maquetacion".equals(service.get("id")))) {
|
||||
ObjectMapper mapperServicio = new ObjectMapper();
|
||||
Object raw = presupuesto.getDatosMaquetacionJson();
|
||||
Map<String, Object> datosMaquetacion;
|
||||
String descripcion = "";
|
||||
try {
|
||||
if (raw instanceof String s) {
|
||||
datosMaquetacion = mapperServicio.readValue(s,
|
||||
new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
} else if (raw instanceof Map<?, ?> m) {
|
||||
datosMaquetacion = mapperServicio.convertValue(m,
|
||||
new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
"Tipo no soportado para datosMaquetacion: "
|
||||
+ raw);
|
||||
}
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException("Error parsing datosMaquetacionJson", e);
|
||||
}
|
||||
descripcion += "<br/><ul><li>";
|
||||
descripcion += (datosMaquetacion.get("num_caracteres") + " "
|
||||
+ messageSource.getMessage("presupuesto.maquetacion.caracteres",
|
||||
null, locale))
|
||||
+ ", ";
|
||||
descripcion += MaquetacionMatrices.Formato
|
||||
.valueOf(datosMaquetacion.get("formato_maquetacion").toString())
|
||||
.getLabel() + ", ";
|
||||
descripcion += messageSource.getMessage(MaquetacionMatrices.FontSize
|
||||
.valueOf(datosMaquetacion.get("cuerpo_texto").toString())
|
||||
.getMessageKey(), null, locale)
|
||||
+ ", ";
|
||||
descripcion += messageSource.getMessage("presupuesto.maquetacion.num-columnas",
|
||||
null, locale) + ": "
|
||||
+ datosMaquetacion.get("num_columnas").toString() + ", ";
|
||||
descripcion += messageSource.getMessage("presupuesto.maquetacion.num-tablas",
|
||||
null, locale) + ": "
|
||||
+ datosMaquetacion.get("num_tablas").toString() + ", ";
|
||||
descripcion += messageSource.getMessage("presupuesto.maquetacion.num-fotos",
|
||||
null, locale) + ": "
|
||||
+ datosMaquetacion.get("num_fotos").toString();
|
||||
if ((boolean) datosMaquetacion.get("correccion_ortotipografica")) {
|
||||
descripcion += ", " + messageSource
|
||||
.getMessage("presupuesto.maquetacion.correccion-ortotipografica",
|
||||
null, locale);
|
||||
}
|
||||
if ((boolean) datosMaquetacion.get("texto_mecanografiado")) {
|
||||
descripcion += ", " + messageSource.getMessage(
|
||||
"presupuesto.maquetacion.texto-mecanografiado",
|
||||
null, locale);
|
||||
}
|
||||
if ((boolean) datosMaquetacion.get("disenio_portada")) {
|
||||
descripcion += ", "
|
||||
+ messageSource.getMessage(
|
||||
"presupuesto.maquetacion.diseno-portada",
|
||||
null, locale);
|
||||
}
|
||||
if ((boolean) datosMaquetacion.get("epub")) {
|
||||
descripcion += ", " + messageSource.getMessage(
|
||||
"presupuesto.maquetacion.epub", null, locale);
|
||||
}
|
||||
descripcion += "</li></ul>";
|
||||
resumen.put("datosMaquetacion", descripcion);
|
||||
}
|
||||
}
|
||||
NumberFormat currencyFormat = NumberFormat.getCurrencyInstance(locale);
|
||||
String formattedString = currencyFormat.format(total.setScale(2, RoundingMode.HALF_UP).doubleValue());
|
||||
resumen.put("total", formattedString);
|
||||
resumen.put("lineas", lineas);
|
||||
resumen.put("servicios", serviciosExtras);
|
||||
return resumen;
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
package com.imprimelibros.erp.common.email;
|
||||
|
||||
import jakarta.mail.MessagingException;
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
|
||||
@ -0,0 +1,77 @@
|
||||
package com.imprimelibros.erp.common.jpa;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import org.springframework.data.annotation.CreatedBy;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.annotation.LastModifiedBy;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import com.imprimelibros.erp.users.User;
|
||||
|
||||
@MappedSuperclass
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public abstract class AbstractAuditedEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
// Auditoría temporal
|
||||
@CreatedDate
|
||||
@Column(name = "created_at", updatable = false)
|
||||
private Instant createdAt;
|
||||
|
||||
@LastModifiedDate
|
||||
@Column(name = "updated_at")
|
||||
private Instant updatedAt;
|
||||
|
||||
// Auditoría por usuario (nullable si público anónimo)
|
||||
@CreatedBy
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "created_by")
|
||||
private User createdBy;
|
||||
|
||||
@LastModifiedBy
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "updated_by")
|
||||
private User updatedBy;
|
||||
|
||||
// Soft delete
|
||||
@Column(name = "deleted", nullable = false)
|
||||
private boolean deleted = false;
|
||||
|
||||
@Column(name = "deleted_at")
|
||||
private Instant deletedAt;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "deleted_by")
|
||||
private User deletedBy;
|
||||
|
||||
// Getters/Setters
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public Instant getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
|
||||
|
||||
public Instant getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
|
||||
|
||||
public User getCreatedBy() { return createdBy; }
|
||||
public void setCreatedBy(User createdBy) { this.createdBy = createdBy; }
|
||||
|
||||
public User getUpdatedBy() { return updatedBy; }
|
||||
public void setUpdatedBy(User updatedBy) { this.updatedBy = updatedBy; }
|
||||
|
||||
public boolean isDeleted() { return deleted; }
|
||||
public void setDeleted(boolean deleted) { this.deleted = deleted; }
|
||||
|
||||
public Instant getDeletedAt() { return deletedAt; }
|
||||
public void setDeletedAt(Instant deletedAt) { this.deletedAt = deletedAt; }
|
||||
|
||||
public User getDeletedBy() { return deletedBy; }
|
||||
public void setDeletedBy(User deletedBy) { this.deletedBy = deletedBy; }
|
||||
}
|
||||
37
src/main/java/com/imprimelibros/erp/common/web/IpUtils.java
Normal file
37
src/main/java/com/imprimelibros/erp/common/web/IpUtils.java
Normal file
@ -0,0 +1,37 @@
|
||||
package com.imprimelibros.erp.common.web;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
public class IpUtils {
|
||||
|
||||
public static String getClientIp(HttpServletRequest request) {
|
||||
String[] headers = {
|
||||
"X-Forwarded-For",
|
||||
"Proxy-Client-IP",
|
||||
"WL-Proxy-Client-IP",
|
||||
"HTTP_X_FORWARDED_FOR",
|
||||
"HTTP_X_FORWARDED",
|
||||
"HTTP_X_CLUSTER_CLIENT_IP",
|
||||
"HTTP_CLIENT_IP",
|
||||
"HTTP_FORWARDED_FOR",
|
||||
"HTTP_FORWARDED",
|
||||
"HTTP_VIA",
|
||||
"REMOTE_ADDR"
|
||||
};
|
||||
|
||||
for (String header : headers) {
|
||||
String ip = request.getHeader(header);
|
||||
if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
|
||||
// Si hay varios (X-Forwarded-For), toma el primero
|
||||
return ip.split(",")[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback
|
||||
String ip = request.getRemoteAddr();
|
||||
if ("0:0:0:0:0:0:0:1".equals(ip) || "::1".equals(ip)) {
|
||||
return "127.0.0.1";
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
// JpaAuditConfig.java
|
||||
package com.imprimelibros.erp.config;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.domain.AuditorAware;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
import com.imprimelibros.erp.users.User;
|
||||
import com.imprimelibros.erp.users.UserDetailsImpl; // tu implementación
|
||||
|
||||
@Configuration
|
||||
@EnableJpaAuditing(auditorAwareRef = "auditorAware")
|
||||
public class JpaAuditConfig {
|
||||
|
||||
@Bean
|
||||
public AuditorAware<User> auditorAware(EntityManager em) {
|
||||
return () -> {
|
||||
var ctx = SecurityContextHolder.getContext();
|
||||
if (ctx == null) return Optional.empty();
|
||||
|
||||
var auth = ctx.getAuthentication();
|
||||
if (auth == null || !auth.isAuthenticated()) return Optional.empty();
|
||||
|
||||
Object principal = auth.getPrincipal();
|
||||
Long userId = null;
|
||||
|
||||
// Tu UserDetailsImpl ya tiene el id
|
||||
if (principal instanceof UserDetailsImpl udi) {
|
||||
userId = udi.getId();
|
||||
}
|
||||
// Si a veces pones el propio User como principal:
|
||||
else if (principal instanceof User u && u.getId() != null) {
|
||||
userId = u.getId();
|
||||
}
|
||||
// ⚠️ NO hagas consultas aquí (nada de userDao.findBy...).
|
||||
if (userId == null) return Optional.empty();
|
||||
|
||||
// Devuelve una referencia gestionada (NO hace SELECT ni fuerza flush)
|
||||
return Optional.of(em.getReference(User.class, userId));
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -85,7 +85,7 @@ public class SecurityConfig {
|
||||
.authenticationProvider(provider)
|
||||
|
||||
.sessionManagement(session -> session
|
||||
.invalidSessionUrl("/login?expired")
|
||||
//.invalidSessionUrl("/login?expired")
|
||||
.maximumSessions(1))
|
||||
|
||||
// Ignora CSRF para tu recurso público (sin Ant/Mvc matchers)
|
||||
@ -113,8 +113,12 @@ public class SecurityConfig {
|
||||
RequestMatcher notStatic = new AndRequestMatcher(
|
||||
new NegatedRequestMatcher(PathRequest.toStaticResources().atCommonLocations()),
|
||||
new NegatedRequestMatcher(pathStartsWith("/assets/")));
|
||||
|
||||
RequestMatcher cartCount = new AndRequestMatcher(
|
||||
new NegatedRequestMatcher(PathRequest.toStaticResources().atCommonLocations()),
|
||||
new NegatedRequestMatcher(pathStartsWith("/cart/count")));
|
||||
|
||||
cache.setRequestMatcher(new AndRequestMatcher(htmlPage, nonAjax, notStatic, notWellKnown));
|
||||
cache.setRequestMatcher(new AndRequestMatcher(htmlPage, nonAjax, notStatic, notWellKnown, cartCount));
|
||||
rc.requestCache(cache);
|
||||
})
|
||||
// ========================================================
|
||||
@ -135,7 +139,8 @@ public class SecurityConfig {
|
||||
"/presupuesto/public/**",
|
||||
"/error",
|
||||
"/favicon.ico",
|
||||
"/.well-known/**" // opcional
|
||||
"/.well-known/**", // opcional
|
||||
"/api/pdf/presupuesto/**"
|
||||
).permitAll()
|
||||
.requestMatchers("/users/**").hasAnyRole("SUPERADMIN", "ADMIN")
|
||||
.anyRequest().authenticated())
|
||||
|
||||
@ -9,8 +9,9 @@ import java.time.LocalDateTime;
|
||||
|
||||
import org.hibernate.annotations.SQLDelete;
|
||||
import org.hibernate.annotations.SQLRestriction;
|
||||
import com.imprimelibros.erp.presupuesto.Presupuesto.TipoEncuadernacion;
|
||||
import com.imprimelibros.erp.presupuesto.Presupuesto.TipoCubierta;
|
||||
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoCubierta;
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoEncuadernacion;
|
||||
import com.imprimelibros.erp.shared.validation.NoRangeOverlap;
|
||||
|
||||
|
||||
|
||||
@ -16,7 +16,6 @@ import org.springframework.stereotype.Controller;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
@ -29,8 +28,8 @@ import com.imprimelibros.erp.datatables.DataTablesParser;
|
||||
import com.imprimelibros.erp.datatables.DataTablesRequest;
|
||||
import com.imprimelibros.erp.datatables.DataTablesResponse;
|
||||
import com.imprimelibros.erp.i18n.TranslationService;
|
||||
import com.imprimelibros.erp.presupuesto.Presupuesto.TipoCubierta;
|
||||
import com.imprimelibros.erp.presupuesto.Presupuesto.TipoEncuadernacion;
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoCubierta;
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoEncuadernacion;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
@ -321,8 +320,6 @@ public class MargenPresupuestoController {
|
||||
|
||||
return repo.findById(id).map(u -> {
|
||||
try {
|
||||
|
||||
|
||||
u.setDeleted(true);
|
||||
u.setDeletedAt(LocalDateTime.now());
|
||||
|
||||
|
||||
@ -5,8 +5,8 @@ import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import com.imprimelibros.erp.presupuesto.Presupuesto.TipoEncuadernacion;
|
||||
import com.imprimelibros.erp.presupuesto.Presupuesto.TipoCubierta;
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoCubierta;
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoEncuadernacion;
|
||||
|
||||
public interface MargenPresupuestoDao
|
||||
extends JpaRepository<MargenPresupuesto, Long>, JpaSpecificationExecutor<MargenPresupuesto> {
|
||||
|
||||
@ -6,8 +6,8 @@ import java.util.Optional;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.imprimelibros.erp.presupuesto.Presupuesto.TipoEncuadernacion;
|
||||
import com.imprimelibros.erp.presupuesto.Presupuesto.TipoCubierta;
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoCubierta;
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoEncuadernacion;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
|
||||
@ -7,12 +7,14 @@ import org.springframework.data.domain.*;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
import jakarta.persistence.criteria.*;
|
||||
import java.util.*;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class DataTable<T> {
|
||||
|
||||
/* ===== Tipos funcionales ===== */
|
||||
public interface FilterHook<T> extends BiConsumer<SpecBuilder<T>, DataTablesRequest> {
|
||||
}
|
||||
|
||||
@ -20,25 +22,55 @@ public class DataTable<T> {
|
||||
void add(Specification<T> extra);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filtro custom por campo virtual: te doy (root, query, cb, value) y me
|
||||
* devuelves un Predicate
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface FieldFilter<T> {
|
||||
Predicate apply(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb, String value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Orden custom por campo virtual: te doy (root, query, cb) y me devuelves la
|
||||
* Expression<?> para orderBy
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface FieldOrder<T> {
|
||||
Expression<?> apply(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);
|
||||
}
|
||||
|
||||
/* ===== Estado ===== */
|
||||
private final JpaSpecificationExecutor<T> repo;
|
||||
private final Class<T> entityClass;
|
||||
private final DataTablesRequest dt;
|
||||
private final List<String> searchable;
|
||||
|
||||
private final List<Function<T, Map<String, Object>>> adders = new ArrayList<>();
|
||||
private final List<Function<Map<String, Object>, Map<String, Object>>> editors = new ArrayList<>();
|
||||
private final List<FilterHook<T>> filters = new ArrayList<>();
|
||||
private Specification<T> baseSpec = (root, q, cb) -> cb.conjunction();
|
||||
|
||||
private final ObjectMapper om = new ObjectMapper()
|
||||
.registerModule(new JavaTimeModule())
|
||||
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||
.registerModule(new JavaTimeModule())
|
||||
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||
|
||||
/** whitelist de campos ordenables “simples” (por nombre) */
|
||||
private List<String> orderable = null;
|
||||
|
||||
/** mapas de comportamiento custom por campo */
|
||||
private final Map<String, FieldOrder<T>> orderCustom = new HashMap<>();
|
||||
private final Map<String, FieldFilter<T>> filterCustom = new HashMap<>();
|
||||
|
||||
private boolean onlyAdded = false;
|
||||
|
||||
/* ===== Ctor / factory ===== */
|
||||
private DataTable(JpaSpecificationExecutor<T> repo, Class<T> entityClass, DataTablesRequest dt,
|
||||
List<String> searchable) {
|
||||
this.repo = repo;
|
||||
this.entityClass = entityClass;
|
||||
this.dt = dt;
|
||||
this.searchable = searchable;
|
||||
this.searchable = searchable != null ? searchable : List.of();
|
||||
}
|
||||
|
||||
public static <T> DataTable<T> of(JpaSpecificationExecutor<T> repo, Class<T> entityClass, DataTablesRequest dt,
|
||||
@ -46,13 +78,19 @@ public class DataTable<T> {
|
||||
return new DataTable<>(repo, entityClass, dt, searchable);
|
||||
}
|
||||
|
||||
/** Equivalente a tu $q->where(...): establece condición base */
|
||||
/* ===== Fluent API ===== */
|
||||
public DataTable<T> onlyAddedColumns() {
|
||||
this.onlyAdded = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** WHERE base reusable */
|
||||
public DataTable<T> where(Specification<T> spec) {
|
||||
this.baseSpec = this.baseSpec.and(spec);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** add("campo", fn(entity)->valor|Map) */
|
||||
/** Campos renderizados */
|
||||
public DataTable<T> add(String field, Function<T, Object> fn) {
|
||||
adders.add(entity -> {
|
||||
Map<String, Object> m = new HashMap<>();
|
||||
@ -62,19 +100,19 @@ public class DataTable<T> {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* add(fn(entity)->Map<String,Object>) para devolver objetos anidados como tu
|
||||
* "logo"
|
||||
*/
|
||||
public DataTable<T> addIf(boolean condition, String field, Function<T, Object> fn) {
|
||||
if (condition)
|
||||
return add(field, fn);
|
||||
return this;
|
||||
}
|
||||
|
||||
public DataTable<T> add(Function<T, Map<String, Object>> fn) {
|
||||
adders.add(fn);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* edit("campo", fn(entity)->valor) sobreescribe un campo existente o lo crea si
|
||||
* no existe
|
||||
*/
|
||||
/** Edita/inyecta valor usando la entidad original (guardada como __entity) */
|
||||
@SuppressWarnings("unchecked")
|
||||
public DataTable<T> edit(String field, Function<T, Object> fn) {
|
||||
editors.add(row -> {
|
||||
row.put(field, fn.apply((T) row.get("__entity")));
|
||||
@ -83,73 +121,132 @@ public class DataTable<T> {
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Whitelist de campos simples ordenables (por nombre) */
|
||||
public DataTable<T> orderable(List<String> fields) {
|
||||
this.orderable = fields;
|
||||
return this;
|
||||
}
|
||||
|
||||
private List<String> getOrderable() {
|
||||
return (orderable == null || orderable.isEmpty()) ? this.searchable : this.orderable;
|
||||
/** Orden custom por campo virtual (expresiones) */
|
||||
public DataTable<T> orderable(String field, FieldOrder<T> orderFn) {
|
||||
this.orderCustom.put(field, orderFn);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** filter((builder, req) -> builder.add(miExtraSpec(req))) */
|
||||
/** Filtro custom por campo virtual (LIKE, rangos, etc.) */
|
||||
public DataTable<T> filter(String field, FieldFilter<T> filterFn) {
|
||||
this.filterCustom.put(field, filterFn);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Hook para añadir Specifications extra programáticamente */
|
||||
public DataTable<T> filter(FilterHook<T> hook) {
|
||||
filters.add(hook);
|
||||
return this;
|
||||
}
|
||||
|
||||
/* ===== Helpers ===== */
|
||||
private List<String> getOrderable() {
|
||||
return (orderable == null || orderable.isEmpty()) ? this.searchable : this.orderable;
|
||||
}
|
||||
|
||||
/* ===== Core ===== */
|
||||
public DataTablesResponse<Map<String, Object>> toJson(long totalCount) {
|
||||
// Construye spec con búsqueda global + base + filtros custom
|
||||
// 1) Spec base + búsqueda (global/columnas) + hooks programáticos
|
||||
Specification<T> spec = baseSpec.and(DataTablesSpecification.build(dt, searchable));
|
||||
final Specification<T>[] holder = new Specification[] { spec };
|
||||
filters.forEach(h -> h.accept(extra -> holder[0] = holder[0].and(extra), dt));
|
||||
spec = holder[0];
|
||||
|
||||
// Sort
|
||||
// Sort
|
||||
// Hooks externos
|
||||
filters.forEach(h -> h.accept(extra -> holder[0] = holder[0].and(extra), dt));
|
||||
|
||||
// 2) Filtros por columna “custom” (virtuales)
|
||||
for (var col : dt.columns) {
|
||||
if (col == null || !col.searchable)
|
||||
continue;
|
||||
if (col.name == null || col.name.isBlank())
|
||||
continue;
|
||||
if (!filterCustom.containsKey(col.name))
|
||||
continue;
|
||||
if (col.search == null || col.search.value == null || col.search.value.isBlank())
|
||||
continue;
|
||||
|
||||
var value = col.search.value;
|
||||
var filterFn = filterCustom.get(col.name);
|
||||
holder[0] = holder[0].and((root, query, cb) -> {
|
||||
Predicate p = filterFn.apply(root, query, cb, value);
|
||||
return p != null ? p : cb.conjunction();
|
||||
});
|
||||
}
|
||||
|
||||
// 3) Orden:
|
||||
// - Para campos “simples” (no custom): con Sort (Spring)
|
||||
// - Para campos “custom” (virtuales/expresiones): query.orderBy(...) dentro de
|
||||
// una spec
|
||||
Sort sort = Sort.unsorted();
|
||||
List<Sort.Order> simpleOrders = new ArrayList<>();
|
||||
boolean customApplied = false;
|
||||
|
||||
if (!dt.order.isEmpty() && !dt.columns.isEmpty()) {
|
||||
List<Sort.Order> orders = new ArrayList<>();
|
||||
for (var o : dt.order) {
|
||||
var col = dt.columns.get(o.column);
|
||||
String field = col != null ? col.name : null;
|
||||
if (col == null)
|
||||
continue;
|
||||
|
||||
String field = col.name;
|
||||
if (field == null || field.isBlank())
|
||||
continue;
|
||||
if (!col.orderable)
|
||||
continue;
|
||||
if (!getOrderable().contains(field))
|
||||
continue; // << usa tu whitelist
|
||||
continue;
|
||||
|
||||
orders.add(new Sort.Order(
|
||||
"desc".equalsIgnoreCase(o.dir) ? Sort.Direction.DESC : Sort.Direction.ASC,
|
||||
field));
|
||||
}
|
||||
if (!orders.isEmpty()) {
|
||||
sort = Sort.by(orders);
|
||||
} else {
|
||||
for (var c : dt.columns) {
|
||||
if (c != null && c.orderable && c.name != null && !c.name.isBlank()
|
||||
&& getOrderable().contains(c.name)) {
|
||||
sort = Sort.by(c.name);
|
||||
break;
|
||||
}
|
||||
if (orderCustom.containsKey(field)) {
|
||||
final boolean asc = !"desc".equalsIgnoreCase(o.dir);
|
||||
final FieldOrder<T> orderFn = orderCustom.get(field);
|
||||
|
||||
// aplica el ORDER BY custom dentro de la Specification (con Criteria)
|
||||
holder[0] = holder[0].and((root, query, cb) -> {
|
||||
Expression<?> expr = orderFn.apply(root, query, cb);
|
||||
if (expr != null) {
|
||||
query.orderBy(asc ? cb.asc(expr) : cb.desc(expr));
|
||||
}
|
||||
return cb.conjunction();
|
||||
});
|
||||
customApplied = true;
|
||||
} else {
|
||||
// orden simple por nombre de propiedad real
|
||||
simpleOrders.add(new Sort.Order(
|
||||
"desc".equalsIgnoreCase(o.dir) ? Sort.Direction.DESC : Sort.Direction.ASC,
|
||||
field));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Page
|
||||
if (!simpleOrders.isEmpty()) {
|
||||
sort = Sort.by(simpleOrders);
|
||||
}
|
||||
|
||||
// 4) Paginación (Sort para simples; custom order ya va dentro de la spec)
|
||||
int page = dt.length > 0 ? dt.start / dt.length : 0;
|
||||
Pageable pageable = dt.length > 0 ? PageRequest.of(page, dt.length, sort) : Pageable.unpaged();
|
||||
|
||||
var p = repo.findAll(holder[0], pageable);
|
||||
long filtered = p.getTotalElements();
|
||||
|
||||
// Mapear entidad -> Map base (via Jackson) + add/edit
|
||||
// 5) Mapeo a Map + add/edit
|
||||
List<Map<String, Object>> data = new ArrayList<>();
|
||||
for (T e : p.getContent()) {
|
||||
Map<String, Object> row = om.convertValue(e, Map.class);
|
||||
row.put("__entity", e); // para editores que necesiten la entidad
|
||||
Map<String, Object> row;
|
||||
if (onlyAdded) {
|
||||
row = new HashMap<>();
|
||||
} else {
|
||||
try {
|
||||
row = om.convertValue(e, Map.class);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
row = new HashMap<>();
|
||||
}
|
||||
}
|
||||
row.put("__entity", e);
|
||||
for (var ad : adders)
|
||||
row.putAll(ad.apply(e));
|
||||
for (var ed : editors)
|
||||
@ -157,6 +254,12 @@ public class DataTable<T> {
|
||||
row.remove("__entity");
|
||||
data.add(row);
|
||||
}
|
||||
|
||||
return new DataTablesResponse<>(dt.draw, totalCount, filtered, data);
|
||||
}
|
||||
}
|
||||
|
||||
private Predicate nullSafePredicate(CriteriaBuilder cb) {
|
||||
// Devuelve conjunción para no interferir con los demás predicados
|
||||
return cb.conjunction();
|
||||
}
|
||||
}
|
||||
@ -23,9 +23,15 @@ public class DataTablesSpecification {
|
||||
DataTablesRequest.Column col = dt.columns.get(i);
|
||||
if (col.searchable && col.search != null && col.search.value != null && !col.search.value.isEmpty()) {
|
||||
try {
|
||||
ands.add(like(cb, root.get(col.name), col.search.value));
|
||||
Path<?> path = root;
|
||||
String[] parts = col.name.split("\\.");
|
||||
for (String part : parts) {
|
||||
path = path.get(part);
|
||||
}
|
||||
ands.add(like(cb, path, col.search.value));
|
||||
} catch (IllegalArgumentException ex) {
|
||||
// columna no mapeada o relación: la ignoramos
|
||||
//System.out.println("[DT] columna no mapeada o relación: " + col.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package com.imprimelibros.erp.externalApi;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
@ -13,13 +14,14 @@ import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.imprimelibros.erp.configuracion.margenes_presupuestos.MargenPresupuesto;
|
||||
import com.imprimelibros.erp.configuracion.margenes_presupuestos.MargenPresupuestoDao;
|
||||
import com.imprimelibros.erp.presupuesto.Presupuesto.TipoCubierta;
|
||||
import com.imprimelibros.erp.presupuesto.Presupuesto.TipoEncuadernacion;
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoCubierta;
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoEncuadernacion;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.Locale;
|
||||
|
||||
@Service
|
||||
public class skApiClient {
|
||||
@ -30,11 +32,13 @@ public class skApiClient {
|
||||
private final AuthService authService;
|
||||
private final RestTemplate restTemplate;
|
||||
private final MargenPresupuestoDao margenPresupuestoDao;
|
||||
private final MessageSource messageSource;
|
||||
|
||||
public skApiClient(AuthService authService, MargenPresupuestoDao margenPresupuestoDao) {
|
||||
public skApiClient(AuthService authService, MargenPresupuestoDao margenPresupuestoDao, MessageSource messageSource) {
|
||||
this.authService = authService;
|
||||
this.restTemplate = new RestTemplate();
|
||||
this.margenPresupuestoDao = margenPresupuestoDao;
|
||||
this.messageSource = messageSource;
|
||||
}
|
||||
|
||||
public String getPrice(Map<String, Object> requestBody, TipoEncuadernacion tipoEncuadernacion,
|
||||
@ -90,7 +94,7 @@ public class skApiClient {
|
||||
margen.getMargenMax(),
|
||||
margen.getMargenMin());
|
||||
double nuevoPrecio = precios.get(i) * (1 + margenValue / 100.0);
|
||||
precios.set(i, nuevoPrecio);
|
||||
precios.set(i, Math.round(nuevoPrecio * 10000.0) / 10000.0); // redondear a 2 decimales
|
||||
} else {
|
||||
System.out.println("No se encontró margen para tirada " + tirada);
|
||||
}
|
||||
@ -113,7 +117,7 @@ public class skApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
public Integer getMaxSolapas(Map<String, Object> requestBody) {
|
||||
public Integer getMaxSolapas(Map<String, Object> requestBody, Locale locale) {
|
||||
try {
|
||||
String jsonResponse = performWithRetry(() -> {
|
||||
String url = this.skApiUrl + "api/calcular-solapas";
|
||||
@ -150,7 +154,7 @@ public class skApiClient {
|
||||
JsonNode root = mapper.readTree(jsonResponse);
|
||||
|
||||
if (root.get("data") == null || !root.get("data").isInt()) {
|
||||
throw new RuntimeException("Respuesta inesperada de calcular-solapas: " + jsonResponse);
|
||||
throw new RuntimeException(messageSource.getMessage("presupuesto.errores.error-interior", new Object[]{1} , locale));
|
||||
}
|
||||
|
||||
return root.get("data").asInt();
|
||||
|
||||
@ -35,19 +35,23 @@ public class HomeController {
|
||||
"presupuesto.plantilla-cubierta",
|
||||
"presupuesto.plantilla-cubierta-text",
|
||||
"presupuesto.impresion-cubierta",
|
||||
"presupuesto.impresion-cubierta-help");
|
||||
"presupuesto.impresion-cubierta-help",
|
||||
"presupuesto.iva-reducido",
|
||||
"presupuesto.iva-reducido-descripcion");
|
||||
|
||||
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||
model.addAttribute("languageBundle", translations);
|
||||
model.addAttribute("pod", variableService.getValorEntero("POD"));
|
||||
model.addAttribute("ancho_alto_min", variableService.getValorEntero("ancho_alto_min"));
|
||||
model.addAttribute("ancho_alto_max", variableService.getValorEntero("ancho_alto_max"));
|
||||
|
||||
model.addAttribute("appMode", "public");
|
||||
}
|
||||
else{
|
||||
// empty translations for authenticated users
|
||||
Map<String, String> translations = Map.of();
|
||||
model.addAttribute("languageBundle", translations);
|
||||
}
|
||||
return "imprimelibros/home";
|
||||
return "imprimelibros/home/home";
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,9 +4,9 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.imprimelibros.erp.presupuesto.Presupuesto.TipoCubierta;
|
||||
import com.imprimelibros.erp.presupuesto.Presupuesto.TipoEncuadernacion;
|
||||
import com.imprimelibros.erp.presupuesto.Presupuesto.TipoImpresion;
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoCubierta;
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoEncuadernacion;
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoImpresion;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
|
||||
11
src/main/java/com/imprimelibros/erp/pdf/DocumentSpec.java
Normal file
11
src/main/java/com/imprimelibros/erp/pdf/DocumentSpec.java
Normal file
@ -0,0 +1,11 @@
|
||||
package com.imprimelibros.erp.pdf;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
public record DocumentSpec(
|
||||
DocumentType type,
|
||||
String templateId, // p.ej. "presupuesto-a4"
|
||||
Locale locale,
|
||||
Map<String, Object> model // data del documento
|
||||
) {}
|
||||
10
src/main/java/com/imprimelibros/erp/pdf/DocumentType.java
Normal file
10
src/main/java/com/imprimelibros/erp/pdf/DocumentType.java
Normal file
@ -0,0 +1,10 @@
|
||||
package com.imprimelibros.erp.pdf;
|
||||
|
||||
public enum DocumentType {
|
||||
PRESUPUESTO, PEDIDO, FACTURA;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name().toLowerCase();
|
||||
}
|
||||
}
|
||||
47
src/main/java/com/imprimelibros/erp/pdf/PdfController.java
Normal file
47
src/main/java/com/imprimelibros/erp/pdf/PdfController.java
Normal file
@ -0,0 +1,47 @@
|
||||
package com.imprimelibros.erp.pdf;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/pdf")
|
||||
public class PdfController {
|
||||
private final PdfService pdfService;
|
||||
|
||||
public PdfController(PdfService pdfService) {
|
||||
this.pdfService = pdfService;
|
||||
}
|
||||
|
||||
@GetMapping(value = "/{type}/{id}", produces = "application/pdf")
|
||||
public ResponseEntity<byte[]> generate(
|
||||
@PathVariable("type") String type,
|
||||
@PathVariable String id,
|
||||
@RequestParam(defaultValue = "inline") String mode,
|
||||
Locale locale) {
|
||||
|
||||
if (type.equals(DocumentType.PRESUPUESTO.toString()) && id == null) {
|
||||
throw new IllegalArgumentException("Falta el ID del presupuesto para generar el PDF");
|
||||
}
|
||||
if (type.equals(DocumentType.PRESUPUESTO.toString())) {
|
||||
Long presupuestoId = Long.valueOf(id);
|
||||
byte[] pdf = pdfService.generaPresupuesto(presupuestoId, locale);
|
||||
var headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_PDF);
|
||||
headers.setContentDisposition(
|
||||
("download".equals(mode)
|
||||
? ContentDisposition.attachment()
|
||||
: ContentDisposition.inline()).filename("presupuesto-" + id + ".pdf").build());
|
||||
|
||||
return new ResponseEntity<>(pdf, headers, HttpStatus.OK);
|
||||
} else {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
15
src/main/java/com/imprimelibros/erp/pdf/PdfModuleConfig.java
Normal file
15
src/main/java/com/imprimelibros/erp/pdf/PdfModuleConfig.java
Normal file
@ -0,0 +1,15 @@
|
||||
// com.imprimelibros.erp.pdf.PdfModuleConfig.java
|
||||
package com.imprimelibros.erp.pdf;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@ConfigurationProperties(prefix = "imprimelibros.pdf")
|
||||
public class PdfModuleConfig {
|
||||
private Map<String, String> templates = new HashMap<>();
|
||||
|
||||
public Map<String, String> getTemplates() { return templates; }
|
||||
public void setTemplates(Map<String, String> templates) { this.templates = templates; }
|
||||
}
|
||||
44
src/main/java/com/imprimelibros/erp/pdf/PdfRenderer.java
Normal file
44
src/main/java/com/imprimelibros/erp/pdf/PdfRenderer.java
Normal file
@ -0,0 +1,44 @@
|
||||
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;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
@Service
|
||||
public class PdfRenderer {
|
||||
|
||||
@Value("classpath:/static/")
|
||||
private org.springframework.core.io.Resource staticRoot;
|
||||
|
||||
public byte[] renderHtmlToPdf(String html) {
|
||||
try (var baos = new ByteArrayOutputStream()) {
|
||||
var builder = new com.openhtmltopdf.pdfboxout.PdfRendererBuilder();
|
||||
builder.useFastMode();
|
||||
|
||||
// 👇 Base URL para que pueda resolver /assets/css/ y /img/
|
||||
builder.withHtmlContent(html, staticRoot.getURL().toString()); // .../target/classes/static/
|
||||
|
||||
|
||||
// (Opcional) Registrar fuentes TTF
|
||||
builder.useFont(() -> getClass().getResourceAsStream("/static/assets/fonts/OpenSans-Regular.ttf"),
|
||||
"Open Sans", 400, BaseRendererBuilder.FontStyle.NORMAL, true);
|
||||
builder.useFont(() -> getClass().getResourceAsStream("/static/assets/fonts/OpenSans-SemiBold.ttf"),
|
||||
"Open Sans", 600, BaseRendererBuilder.FontStyle.NORMAL, true);
|
||||
builder.useFont(() -> getClass().getResourceAsStream("/static/assets/fonts/OpenSans-Bold.ttf"),
|
||||
"Open Sans", 700, BaseRendererBuilder.FontStyle.NORMAL, true);
|
||||
|
||||
builder.toStream(baos);
|
||||
builder.run();
|
||||
|
||||
return baos.toByteArray();
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Error generando PDF", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
205
src/main/java/com/imprimelibros/erp/pdf/PdfService.java
Normal file
205
src/main/java/com/imprimelibros/erp/pdf/PdfService.java
Normal file
@ -0,0 +1,205 @@
|
||||
package com.imprimelibros.erp.pdf;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.imprimelibros.erp.presupuesto.PresupuestoRepository;
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||
|
||||
import com.imprimelibros.erp.common.Utils;
|
||||
|
||||
@Service
|
||||
public class PdfService {
|
||||
private final TemplateRegistry registry;
|
||||
private final PdfTemplateEngine engine;
|
||||
private final PdfRenderer renderer;
|
||||
private final PresupuestoRepository presupuestoRepository;
|
||||
private final Utils utils;
|
||||
|
||||
private final Map<String, String> empresa = Map.of(
|
||||
"nombre", "ImprimeLibros ERP",
|
||||
"direccion", "C/ Dirección 123, 28000 Madrid",
|
||||
"telefono", "+34 600 000 000",
|
||||
"email", "info@imprimelibros.com",
|
||||
"cif", "B-12345678",
|
||||
"cp", "28000",
|
||||
"poblacion", "Madrid",
|
||||
"web", "www.imprimelibros.com");
|
||||
|
||||
|
||||
private static class PrecioTirada {
|
||||
private Double peso;
|
||||
@JsonProperty("iva_importe_4")
|
||||
private Double ivaImporte4;
|
||||
@JsonProperty("total_con_iva")
|
||||
private Double totalConIva;
|
||||
@JsonProperty("base_imponible")
|
||||
private Double baseImponible;
|
||||
@JsonProperty("iva_importe_21")
|
||||
private Double ivaImporte21;
|
||||
@JsonProperty("precio_unitario")
|
||||
private Double precioUnitario;
|
||||
@JsonProperty("servicios_total")
|
||||
private Double serviciosTotal;
|
||||
@JsonProperty("precio_total_tirada")
|
||||
private Double precioTotalTirada;
|
||||
|
||||
public Double getPeso() {
|
||||
return peso;
|
||||
}
|
||||
|
||||
public Double getIvaImporte4() {
|
||||
return ivaImporte4;
|
||||
}
|
||||
|
||||
public Double getTotalConIva() {
|
||||
return totalConIva;
|
||||
}
|
||||
|
||||
public Double getBaseImponible() {
|
||||
return baseImponible;
|
||||
}
|
||||
|
||||
public Double getIvaImporte21() {
|
||||
return ivaImporte21;
|
||||
}
|
||||
|
||||
public Double getPrecioUnitario() {
|
||||
return precioUnitario;
|
||||
}
|
||||
|
||||
public Double getServiciosTotal() {
|
||||
return serviciosTotal;
|
||||
}
|
||||
|
||||
public Double getPrecioTotalTirada() {
|
||||
return precioTotalTirada;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public PdfService(TemplateRegistry registry, PdfTemplateEngine engine, PdfRenderer renderer,
|
||||
PresupuestoRepository presupuestoRepository, Utils utils) {
|
||||
this.registry = registry;
|
||||
this.engine = engine;
|
||||
this.renderer = renderer;
|
||||
this.presupuestoRepository = presupuestoRepository;
|
||||
this.utils = utils;
|
||||
}
|
||||
|
||||
private byte[] generate(DocumentSpec spec) {
|
||||
var template = registry.resolve(spec.type(), spec.templateId());
|
||||
if (template == null) {
|
||||
throw new IllegalArgumentException("Plantilla no registrada: " + spec.type() + ":" + spec.templateId());
|
||||
}
|
||||
var html = engine.render(template, spec.locale(), spec.model());
|
||||
return renderer.renderHtmlToPdf(html);
|
||||
}
|
||||
|
||||
public byte[] generaPresupuesto(Long presupuestoId, Locale locale) {
|
||||
|
||||
try {
|
||||
Presupuesto presupuesto = presupuestoRepository.findById(presupuestoId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Presupuesto no encontrado: " + presupuestoId));
|
||||
|
||||
Map<String, Object> model = new HashMap<>();
|
||||
model.put("numero", presupuesto.getId());
|
||||
model.put("fecha", presupuesto.getUpdatedAt());
|
||||
|
||||
model.put("empresa", empresa);
|
||||
|
||||
model.put("cliente", Map.of(
|
||||
"nombre", presupuesto.getUser() != null ? presupuesto.getUser().getFullName() : ""));
|
||||
|
||||
model.put("titulo", presupuesto.getTitulo());
|
||||
|
||||
/*
|
||||
* Map<String, Object> resumen = presupuestoService.getTextosResumen(
|
||||
* presupuesto, null, model, model, null)
|
||||
*/
|
||||
model.put("lineas", List.of(
|
||||
Map.of("descripcion", "Impresión interior B/N offset 80 g",
|
||||
"meta", "300 páginas · tinta negra · papel 80 g",
|
||||
"uds", 1000,
|
||||
"precio", 2.15,
|
||||
"dto", 0,
|
||||
"importe", 2150.0),
|
||||
Map.of("descripcion", "Cubierta color 300 g laminado mate",
|
||||
"meta", "Lomo 15 mm · 4/0 · laminado mate",
|
||||
"uds", 1000,
|
||||
"precio", 0.38,
|
||||
"dto", 5.0,
|
||||
"importe", 361.0)));
|
||||
|
||||
model.put("servicios", List.of(
|
||||
Map.of("descripcion", "Transporte península", "unidades", 1, "precio", 90.00)));
|
||||
|
||||
Map<String, Object> specs = utils.getTextoPresupuesto(presupuesto, locale);
|
||||
model.put("specs", specs);
|
||||
|
||||
Map<String, Object> pricing = new HashMap<>();
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
// Si quieres parsear directamente a un Map:
|
||||
Map<Integer, PrecioTirada> snapshot = mapper.readValue(presupuesto.getPricingSnapshotJson(),
|
||||
mapper.getTypeFactory().constructMapType(Map.class, Integer.class, PrecioTirada.class));
|
||||
List<Integer> tiradas = snapshot.keySet().stream().toList();
|
||||
pricing.put("tiradas", tiradas);
|
||||
pricing.put("impresion", snapshot.values().stream()
|
||||
.map(p -> Utils.formatCurrency(p.getPrecioTotalTirada(), locale))
|
||||
.toList());
|
||||
pricing.put("servicios", snapshot.values().stream()
|
||||
.map(p -> Utils.formatCurrency(p.getServiciosTotal(), locale))
|
||||
.toList());
|
||||
pricing.put("peso", snapshot.values().stream()
|
||||
.map(p -> Utils.formatCurrency(p.getPeso(), locale))
|
||||
.toList());
|
||||
pricing.put("iva_4", snapshot.values().stream()
|
||||
.map(p -> Utils.formatCurrency(p.getIvaImporte4(), locale))
|
||||
.toList());
|
||||
pricing.put("iva_21", snapshot.values().stream()
|
||||
.map(p -> Utils.formatCurrency(p.getIvaImporte21(), locale))
|
||||
.toList());
|
||||
pricing.put("total", snapshot.values().stream()
|
||||
.map(p -> Utils.formatCurrency(p.getTotalConIva(), locale))
|
||||
.toList());
|
||||
pricing.put("show_iva_4", presupuesto.getIvaImporte4().floatValue() > 0);
|
||||
pricing.put("show_iva_21", presupuesto.getIvaImporte21().floatValue() > 0);
|
||||
model.put("pricing", pricing);
|
||||
|
||||
var spec = new DocumentSpec(
|
||||
DocumentType.PRESUPUESTO,
|
||||
"presupuesto-a4",
|
||||
Locale.forLanguageTag("es-ES"),
|
||||
model);
|
||||
|
||||
byte[] pdf = this.generate(spec);
|
||||
|
||||
// HTML
|
||||
// (Opcional) generar HTML de depuración con CSS incrustado
|
||||
try {
|
||||
String templateName = registry.resolve(DocumentType.PRESUPUESTO, "presupuesto-a4");
|
||||
String html = engine.render(templateName, Locale.forLanguageTag("es-ES"), model);
|
||||
String css = Files.readString(Path.of("src/main/resources/static/assets/css/presupuestopdf.css"));
|
||||
String htmlWithCss = html.replaceFirst("(?i)</head>", "<style>\n" + css + "\n</style>\n</head>");
|
||||
Path htmlPath = Path.of("target/presupuesto-test.html");
|
||||
Files.writeString(htmlPath, htmlWithCss, StandardCharsets.UTF_8);
|
||||
} catch (Exception ignore) {
|
||||
/* solo para depuración */ }
|
||||
|
||||
return pdf;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Error generando presupuesto PDF", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package com.imprimelibros.erp.pdf;
|
||||
|
||||
import org.thymeleaf.context.Context;
|
||||
import org.thymeleaf.spring6.SpringTemplateEngine;
|
||||
import org.springframework.stereotype.Service;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class PdfTemplateEngine {
|
||||
private final SpringTemplateEngine thymeleaf;
|
||||
|
||||
public PdfTemplateEngine(SpringTemplateEngine thymeleaf) {
|
||||
this.thymeleaf = thymeleaf;
|
||||
}
|
||||
|
||||
public String render(String templateName, Locale locale, Map<String,Object> model) {
|
||||
Context ctx = new Context(locale);
|
||||
if (model != null) model.forEach(ctx::setVariable);
|
||||
return thymeleaf.process(templateName, ctx);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package com.imprimelibros.erp.pdf;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class TemplateRegistry {
|
||||
private final PdfModuleConfig config;
|
||||
|
||||
public TemplateRegistry(PdfModuleConfig config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public String resolve(DocumentType type, String templateId) {
|
||||
String key = type.name() + ":" + templateId;
|
||||
String keyAlt = type.name() + "_" + templateId; // compatibilidad con properties
|
||||
if (config.getTemplates() == null) return null;
|
||||
String value = config.getTemplates().get(key);
|
||||
if (value == null) value = config.getTemplates().get(keyAlt);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
package com.imprimelibros.erp.presupuesto;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface GeoIpService {
|
||||
|
||||
class GeoData {
|
||||
public final String pais;
|
||||
public final String region;
|
||||
public final String ciudad;
|
||||
|
||||
public GeoData(String pais, String region, String ciudad) {
|
||||
this.pais = pais;
|
||||
this.region = region;
|
||||
this.ciudad = ciudad;
|
||||
}
|
||||
|
||||
public String getPais() {
|
||||
return pais;
|
||||
}
|
||||
|
||||
public String getRegion() {
|
||||
return region;
|
||||
}
|
||||
|
||||
public String getCiudad() {
|
||||
return ciudad;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ip Ip original (no anonimizada) - la implementación debe manejar IPv4/IPv6.
|
||||
* @return GeoData si se pudo resolver; Optional.empty() en caso de error o IP privada.
|
||||
*/
|
||||
Optional<GeoData> lookup(String ip);
|
||||
}
|
||||
@ -1,40 +1,68 @@
|
||||
package com.imprimelibros.erp.presupuesto;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.ui.Model;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import jakarta.validation.Validator;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.imprimelibros.erp.configurationERP.VariableService;
|
||||
import com.imprimelibros.erp.datatables.*;
|
||||
import com.imprimelibros.erp.externalApi.skApiClient;
|
||||
import com.imprimelibros.erp.i18n.TranslationService;
|
||||
import com.imprimelibros.erp.presupuesto.classes.ImagenPresupuesto;
|
||||
import com.imprimelibros.erp.presupuesto.classes.PresupuestoMaquetacion;
|
||||
import com.imprimelibros.erp.presupuesto.classes.PresupuestoMarcapaginas;
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||
import com.imprimelibros.erp.presupuesto.service.PresupuestoService;
|
||||
import com.imprimelibros.erp.presupuesto.validation.PresupuestoValidationGroups;
|
||||
import com.imprimelibros.erp.users.UserDao;
|
||||
import com.imprimelibros.erp.users.UserDetailsImpl;
|
||||
import com.imprimelibros.erp.presupuesto.service.PresupuestoFormDataMapper;
|
||||
import com.imprimelibros.erp.presupuesto.service.PresupuestoFormDataMapper.PresupuestoFormDataDto;
|
||||
import com.imprimelibros.erp.common.web.IpUtils;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/presupuesto")
|
||||
public class PresupuestoController {
|
||||
|
||||
private final PresupuestoRepository presupuestoRepository;
|
||||
|
||||
@Autowired
|
||||
protected PresupuestoService presupuestoService;
|
||||
|
||||
@ -44,10 +72,26 @@ public class PresupuestoController {
|
||||
@Autowired
|
||||
protected MessageSource messageSource;
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
@Autowired
|
||||
private Validator validator;
|
||||
|
||||
public PresupuestoController(ObjectMapper objectMapper) {
|
||||
private final ObjectMapper objectMapper;
|
||||
private final TranslationService translationService;
|
||||
private final PresupuestoDatatableService dtService;
|
||||
private final VariableService variableService;
|
||||
private final PresupuestoFormDataMapper formDataMapper;
|
||||
private final UserDao userRepo;
|
||||
|
||||
public PresupuestoController(ObjectMapper objectMapper, TranslationService translationService,
|
||||
PresupuestoDatatableService dtService, PresupuestoRepository presupuestoRepository,
|
||||
VariableService variableService, PresupuestoFormDataMapper formDataMapper, UserDao userRepo) {
|
||||
this.objectMapper = objectMapper;
|
||||
this.translationService = translationService;
|
||||
this.dtService = dtService;
|
||||
this.presupuestoRepository = presupuestoRepository;
|
||||
this.variableService = variableService;
|
||||
this.formDataMapper = formDataMapper;
|
||||
this.userRepo = userRepo;
|
||||
}
|
||||
|
||||
@PostMapping("/public/validar/datos-generales")
|
||||
@ -57,12 +101,14 @@ public class PresupuestoController {
|
||||
|
||||
Map<String, String> errores = new HashMap<>();
|
||||
|
||||
// errores de campos individuales
|
||||
result.getFieldErrors().forEach(error -> errores.put(error.getField(), error.getDefaultMessage()));
|
||||
|
||||
// errores globales (@ConsistentTiradas...)
|
||||
result.getGlobalErrors().forEach(error -> errores.put("global", error.getDefaultMessage()));
|
||||
|
||||
result.getFieldErrors().forEach(error -> {
|
||||
String code = Objects.requireNonNullElse(error.getDefaultMessage(), "").replace("{", "").replace("}", "");
|
||||
String msg = messageSource.getMessage(code, null, locale);
|
||||
errores.put(error.getField(), msg);
|
||||
});
|
||||
result.getGlobalErrors().forEach(error -> {
|
||||
errores.put("global", error.getDefaultMessage());
|
||||
});
|
||||
if (!errores.isEmpty()) {
|
||||
return ResponseEntity.badRequest().body(errores);
|
||||
}
|
||||
@ -86,7 +132,11 @@ public class PresupuestoController {
|
||||
Map<String, String> errores = new HashMap<>();
|
||||
|
||||
// errores de campos individuales
|
||||
result.getFieldErrors().forEach(error -> errores.put(error.getField(), error.getDefaultMessage()));
|
||||
result.getFieldErrors().forEach(error -> {
|
||||
String code = Objects.requireNonNullElse(error.getDefaultMessage(), "").replace("{", "").replace("}", "");
|
||||
String msg = messageSource.getMessage(code, null, locale);
|
||||
errores.put(error.getField(), msg);
|
||||
});
|
||||
|
||||
// errores globales (@ConsistentTiradas...)
|
||||
result.getGlobalErrors().forEach(error -> errores.put("global", error.getDefaultMessage()));
|
||||
@ -95,7 +145,7 @@ public class PresupuestoController {
|
||||
return ResponseEntity.badRequest().body(errores);
|
||||
}
|
||||
Map<String, Object> resultado = new HashMap<>();
|
||||
resultado.put("solapas", apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto)));
|
||||
resultado.put("solapas", apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto), locale));
|
||||
resultado.putAll(presupuestoService.obtenerOpcionesAcabadosCubierta(presupuesto, locale));
|
||||
return ResponseEntity.ok(resultado);
|
||||
}
|
||||
@ -110,7 +160,11 @@ public class PresupuestoController {
|
||||
Map<String, String> errores = new HashMap<>();
|
||||
|
||||
// errores de campos individuales
|
||||
result.getFieldErrors().forEach(error -> errores.put(error.getField(), error.getDefaultMessage()));
|
||||
result.getFieldErrors().forEach(error -> {
|
||||
String code = Objects.requireNonNullElse(error.getDefaultMessage(), "").replace("{", "").replace("}", "");
|
||||
String msg = messageSource.getMessage(code, null, locale);
|
||||
errores.put(error.getField(), msg);
|
||||
});
|
||||
|
||||
// errores globales (@ConsistentTiradas...)
|
||||
result.getGlobalErrors().forEach(error -> errores.put("global", error.getDefaultMessage()));
|
||||
@ -141,7 +195,11 @@ public class PresupuestoController {
|
||||
Map<String, String> errores = new HashMap<>();
|
||||
|
||||
// errores de campos individuales
|
||||
result.getFieldErrors().forEach(error -> errores.put(error.getField(), error.getDefaultMessage()));
|
||||
result.getFieldErrors().forEach(error -> {
|
||||
String code = Objects.requireNonNullElse(error.getDefaultMessage(), "").replace("{", "").replace("}", "");
|
||||
String msg = messageSource.getMessage(code, null, locale);
|
||||
errores.put(error.getField(), msg);
|
||||
});
|
||||
|
||||
// errores globales (@ConsistentTiradas...)
|
||||
result.getGlobalErrors().forEach(error -> errores.put("global", error.getDefaultMessage()));
|
||||
@ -168,7 +226,11 @@ public class PresupuestoController {
|
||||
Map<String, String> errores = new HashMap<>();
|
||||
|
||||
// errores de campos individuales
|
||||
result.getFieldErrors().forEach(error -> errores.put(error.getField(), error.getDefaultMessage()));
|
||||
result.getFieldErrors().forEach(error -> {
|
||||
String code = Objects.requireNonNullElse(error.getDefaultMessage(), "").replace("{", "").replace("}", "");
|
||||
String msg = messageSource.getMessage(code, null, locale);
|
||||
errores.put(error.getField(), msg);
|
||||
});
|
||||
|
||||
if (!errores.isEmpty()) {
|
||||
return ResponseEntity.badRequest().body(errores);
|
||||
@ -180,7 +242,19 @@ public class PresupuestoController {
|
||||
// opciones gramaje interior
|
||||
resultado.putAll(presupuestoService.obtenerOpcionesGramajeInterior(presupuesto));
|
||||
|
||||
List<String> opciones = new ObjectMapper().convertValue(resultado.get("opciones_papel_interior"),
|
||||
List<ImagenPresupuesto> opciones_papel = new ObjectMapper().convertValue(
|
||||
presupuestoService
|
||||
.obtenerOpcionesPapelInterior(presupuesto, locale)
|
||||
.get("opciones_papel_interior"),
|
||||
new TypeReference<List<ImagenPresupuesto>>() {
|
||||
});
|
||||
|
||||
if (opciones_papel != null && opciones_papel.stream().noneMatch(
|
||||
o -> o.getExtra_data().get("sk-id").equals(String.valueOf(presupuesto.getPapelInteriorId())))) {
|
||||
presupuesto.setPapelInteriorId(Integer.valueOf(opciones_papel.get(0).getExtra_data().get("sk-id")));
|
||||
}
|
||||
|
||||
List<String> opciones = new ObjectMapper().convertValue(resultado.get("opciones_gramaje_interior"),
|
||||
new TypeReference<List<String>>() {
|
||||
});
|
||||
|
||||
@ -191,19 +265,23 @@ public class PresupuestoController {
|
||||
}
|
||||
}
|
||||
|
||||
resultado.put("solapas", apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto)));
|
||||
resultado.put("solapas", apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto), locale));
|
||||
return ResponseEntity.ok(resultado);
|
||||
}
|
||||
|
||||
@PostMapping("/public/get-gramaje-interior")
|
||||
public ResponseEntity<?> getGramajeInterior(
|
||||
@Validated(PresupuestoValidationGroups.Interior.class) Presupuesto presupuesto,
|
||||
BindingResult result) {
|
||||
BindingResult result, Locale locale) {
|
||||
|
||||
Map<String, String> errores = new HashMap<>();
|
||||
|
||||
// errores de campos individuales
|
||||
result.getFieldErrors().forEach(error -> errores.put(error.getField(), error.getDefaultMessage()));
|
||||
result.getFieldErrors().forEach(error -> {
|
||||
String code = Objects.requireNonNullElse(error.getDefaultMessage(), "").replace("{", "").replace("}", "");
|
||||
String msg = messageSource.getMessage(code, null, locale);
|
||||
errores.put(error.getField(), msg);
|
||||
});
|
||||
|
||||
if (!errores.isEmpty()) {
|
||||
return ResponseEntity.badRequest().body(errores);
|
||||
@ -220,26 +298,30 @@ public class PresupuestoController {
|
||||
presupuesto.setGramajeInterior(Integer.parseInt(opciones.get(0))); // Asignar primera opción
|
||||
}
|
||||
}
|
||||
resultado.put("solapas", apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto)));
|
||||
resultado.put("solapas", apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto), locale));
|
||||
return ResponseEntity.ok(resultado);
|
||||
}
|
||||
|
||||
@PostMapping("/public/get-max-solapas")
|
||||
public ResponseEntity<?> getMaxSolapas(
|
||||
@Validated(PresupuestoValidationGroups.Interior.class) Presupuesto presupuesto,
|
||||
BindingResult result) {
|
||||
BindingResult result, Locale locale) {
|
||||
|
||||
Map<String, String> errores = new HashMap<>();
|
||||
|
||||
// errores de campos individuales
|
||||
result.getFieldErrors().forEach(error -> errores.put(error.getField(), error.getDefaultMessage()));
|
||||
result.getFieldErrors().forEach(error -> {
|
||||
String code = Objects.requireNonNullElse(error.getDefaultMessage(), "").replace("{", "").replace("}", "");
|
||||
String msg = messageSource.getMessage(code, null, locale);
|
||||
errores.put(error.getField(), msg);
|
||||
});
|
||||
|
||||
if (!errores.isEmpty()) {
|
||||
return ResponseEntity.badRequest().body(errores);
|
||||
}
|
||||
|
||||
Map<String, Object> resultado = new HashMap<>();
|
||||
resultado.put("solapas", apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto)));
|
||||
resultado.put("solapas", apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto), locale));
|
||||
return ResponseEntity.ok(resultado);
|
||||
}
|
||||
|
||||
@ -388,13 +470,298 @@ public class PresupuestoController {
|
||||
|
||||
// Se hace un post para no tener problemas con la longitud de la URL
|
||||
@PostMapping("/public/resumen")
|
||||
public ResponseEntity<?> getResumen(@RequestBody Map<String, Object> body, Locale locale) {
|
||||
Presupuesto p = objectMapper.convertValue(body.get("presupuesto"), Presupuesto.class);
|
||||
public ResponseEntity<?> getResumen(
|
||||
@RequestBody Map<String, Object> body,
|
||||
Locale locale,
|
||||
HttpServletRequest request) {
|
||||
|
||||
Presupuesto p = objectMapper.convertValue(body.get("presupuesto"), Presupuesto.class);
|
||||
Boolean save = objectMapper.convertValue(body.get("save"), Boolean.class);
|
||||
String mode = objectMapper.convertValue(body.get("mode"), String.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> serviciosList = (List<Map<String, Object>>) body.getOrDefault("servicios", List.of());
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> datosMaquetacion = (Map<String, Object>) objectMapper
|
||||
.convertValue(body.get("datosMaquetacion"), Map.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> datosMarcapaginas = (Map<String, Object>) objectMapper
|
||||
.convertValue(body.get("datosMarcapaginas"), Map.class);
|
||||
|
||||
return ResponseEntity.ok(presupuestoService.getResumen(p, serviciosList, locale));
|
||||
String sessionId = request.getSession(true).getId();
|
||||
String ip = IpUtils.getClientIp(request);
|
||||
|
||||
var resumen = presupuestoService.getResumen(p, serviciosList, datosMaquetacion, datosMarcapaginas, save, mode, locale, sessionId, ip);
|
||||
|
||||
return ResponseEntity.ok(resumen);
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// MÉTODOS PARA USUARIOS AUTENTICADOS
|
||||
// =============================================
|
||||
@GetMapping
|
||||
public String getPresupuestoList(Model model, Authentication authentication, Locale locale) {
|
||||
|
||||
List<String> keys = List.of(
|
||||
"presupuesto.delete.title",
|
||||
"presupuesto.delete.text",
|
||||
"presupuesto.eliminar",
|
||||
"presupuesto.delete.button",
|
||||
"app.yes",
|
||||
"app.cancelar",
|
||||
"presupuesto.delete.ok.title",
|
||||
"presupuesto.delete.ok.text",
|
||||
"presupuesto.add.tipo",
|
||||
"presupuesto.add.anonimo",
|
||||
"presupuesto.add.cliente",
|
||||
"presupuesto.add.next",
|
||||
"presupuesto.add.cancel",
|
||||
"presupuesto.add.select-client",
|
||||
"presupuesto.add.error.options",
|
||||
"presupuesto.add.error.options-client");
|
||||
|
||||
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||
model.addAttribute("languageBundle", translations);
|
||||
|
||||
return "imprimelibros/presupuestos/presupuesto-list";
|
||||
}
|
||||
|
||||
@GetMapping(value = { "/edit/{id}", "/view/{id}" })
|
||||
public String getPresupuestoEditForm(
|
||||
@PathVariable(name = "id", required = true) Long id,
|
||||
RedirectAttributes redirectAttributes,
|
||||
Model model,
|
||||
Authentication authentication,
|
||||
Locale locale) {
|
||||
|
||||
List<String> keys = List.of(
|
||||
"presupuesto.plantilla-cubierta",
|
||||
"presupuesto.plantilla-cubierta-text",
|
||||
"presupuesto.impresion-cubierta",
|
||||
"presupuesto.impresion-cubierta-help",
|
||||
"presupuesto.exito.guardado",
|
||||
"presupuesto.add.error.save.title",
|
||||
"presupuesto.iva-reducido",
|
||||
"presupuesto.iva-reducido-descripcion");
|
||||
|
||||
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||
model.addAttribute("languageBundle", translations);
|
||||
model.addAttribute("pod", variableService.getValorEntero("POD"));
|
||||
model.addAttribute("ancho_alto_min", variableService.getValorEntero("ancho_alto_min"));
|
||||
model.addAttribute("ancho_alto_max", variableService.getValorEntero("ancho_alto_max"));
|
||||
|
||||
// Buscar el presupuesto
|
||||
Optional<Presupuesto> presupuestoOpt = presupuestoRepository.findById(id);
|
||||
|
||||
if (presupuestoOpt.isEmpty()) {
|
||||
// Añadir mensaje flash para mostrar alerta
|
||||
redirectAttributes.addFlashAttribute("errorMessage",
|
||||
messageSource.getMessage("presupuesto.errores.presupuesto-no-existe", new Object[] { id }, locale));
|
||||
// Redirigir a la vista de lista
|
||||
return "redirect:/presupuesto";
|
||||
}
|
||||
|
||||
if (!presupuestoService.canAccessPresupuesto(presupuestoOpt.get(), authentication)) {
|
||||
// Añadir mensaje flash para mostrar alerta
|
||||
redirectAttributes.addFlashAttribute("errorMessage",
|
||||
messageSource.getMessage("app.errors.403", null, locale));
|
||||
// Redirigir a la vista de lista
|
||||
return "redirect:/presupuesto";
|
||||
}
|
||||
|
||||
model.addAttribute("presupuesto_id", presupuestoOpt.get().getId());
|
||||
String path = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
|
||||
.getRequest().getRequestURI();
|
||||
String mode = path.contains("/view/") ? "view" : "edit";
|
||||
if (mode.equals("view")) {
|
||||
model.addAttribute("appMode", "view");
|
||||
} else {
|
||||
model.addAttribute("cliente_id", presupuestoOpt.get().getUser().getId());
|
||||
model.addAttribute("appMode", "edit");
|
||||
}
|
||||
model.addAttribute("id", presupuestoOpt.get().getId());
|
||||
return "imprimelibros/presupuestos/presupuesto-form";
|
||||
}
|
||||
|
||||
@GetMapping(value = { "/add/{mode}", "/add/{mode}/{cliente_id}", "/add2/{cliente_id}" })
|
||||
public String getPresupuestoEditForm(
|
||||
@PathVariable(name = "mode", required = false) String mode,
|
||||
@PathVariable(name = "cliente_id", required = false) Long clienteId,
|
||||
RedirectAttributes redirectAttributes,
|
||||
Model model,
|
||||
Authentication authentication,
|
||||
Locale locale) {
|
||||
|
||||
List<String> keys = List.of(
|
||||
"presupuesto.plantilla-cubierta",
|
||||
"presupuesto.plantilla-cubierta-text",
|
||||
"presupuesto.impresion-cubierta",
|
||||
"presupuesto.impresion-cubierta-help",
|
||||
"presupuesto.exito.guardado",
|
||||
"presupuesto.add.error.save.title",
|
||||
"presupuesto.iva-reducido",
|
||||
"presupuesto.iva-reducido-descripcion");
|
||||
|
||||
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||
model.addAttribute("languageBundle", translations);
|
||||
model.addAttribute("pod", variableService.getValorEntero("POD"));
|
||||
model.addAttribute("ancho_alto_min", variableService.getValorEntero("ancho_alto_min"));
|
||||
model.addAttribute("ancho_alto_max", variableService.getValorEntero("ancho_alto_max"));
|
||||
|
||||
model.addAttribute("appMode", "add");
|
||||
|
||||
if (!mode.equals("public")) {
|
||||
model.addAttribute("cliente_id", clienteId);
|
||||
}
|
||||
|
||||
model.addAttribute("mode", mode);
|
||||
return "imprimelibros/presupuestos/presupuesto-form";
|
||||
}
|
||||
|
||||
@GetMapping(value = "/datatable/{tipo}", produces = "application/json")
|
||||
@ResponseBody
|
||||
public DataTablesResponse<Map<String, Object>> datatable(
|
||||
HttpServletRequest request, Authentication auth, Locale locale,
|
||||
@PathVariable("tipo") String tipo) {
|
||||
|
||||
DataTablesRequest dt = DataTablesParser.from(request);
|
||||
|
||||
if ("anonimos".equals(tipo)) {
|
||||
return dtService.datatablePublicos(dt, locale);
|
||||
} else if ("clientes".equals(tipo)) {
|
||||
return dtService.datatablePrivados(dt, locale);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Tipo de datatable no válido");
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@Transactional
|
||||
public ResponseEntity<?> delete(@PathVariable Long id, Authentication auth, Locale locale) {
|
||||
|
||||
Presupuesto p = presupuestoRepository.findById(id).orElse(null);
|
||||
if (p == null) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body(Map.of("message", messageSource.getMessage("presupuesto.error.not-found", null, locale)));
|
||||
}
|
||||
|
||||
boolean isUser = auth != null && auth.getAuthorities().stream()
|
||||
.anyMatch(a -> a.getAuthority().equals("ROLE_USER"));
|
||||
|
||||
Long ownerId = p.getUser() != null ? p.getUser().getId() : null;
|
||||
Long currentUserId = null;
|
||||
if (auth != null && auth.getPrincipal() instanceof UserDetailsImpl udi) {
|
||||
currentUserId = udi.getId();
|
||||
} else if (auth != null) {
|
||||
currentUserId = userRepo.findIdByUserNameIgnoreCase(auth.getName()).orElse(null);
|
||||
}
|
||||
boolean isOwner = ownerId != null && ownerId.equals(currentUserId);
|
||||
|
||||
if (isUser && !isOwner) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(Map.of("message",
|
||||
messageSource.getMessage("presupuesto.error.delete-permission-denied", null, locale)));
|
||||
}
|
||||
|
||||
if (p.getEstado() != null && !p.getEstado().equals(Presupuesto.Estado.borrador)) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(Map.of("message",
|
||||
messageSource.getMessage("presupuesto.error.delete-not-draft", null, locale)));
|
||||
}
|
||||
|
||||
try {
|
||||
p.setDeleted(true);
|
||||
p.setDeletedAt(Instant.now());
|
||||
|
||||
if (auth != null && auth.getPrincipal() instanceof UserDetailsImpl udi) {
|
||||
p.setDeletedBy(userRepo.getReferenceById(udi.getId()));
|
||||
} else if (auth != null) {
|
||||
userRepo.findByUserNameIgnoreCase(auth.getName()).ifPresent(p::setDeletedBy);
|
||||
}
|
||||
presupuestoRepository.saveAndFlush(p);
|
||||
|
||||
return ResponseEntity.ok(Map.of("message",
|
||||
messageSource.getMessage("presupuesto.exito.eliminado", null, locale)));
|
||||
|
||||
} catch (Exception ex) {
|
||||
// Devuelve SIEMPRE algo en el catch
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(Map.of("message",
|
||||
messageSource.getMessage("presupuesto.error.delete-internal-error", null, locale),
|
||||
"detail",
|
||||
ex.getClass().getSimpleName() + ": " + (ex.getMessage() != null ? ex.getMessage() : "")));
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping(value = "/api/get", produces = "application/json")
|
||||
public ResponseEntity<PresupuestoFormDataDto> getPresupuesto(
|
||||
@RequestParam("id") Long id, Authentication authentication) {
|
||||
|
||||
Optional<Presupuesto> presupuestoOpt = presupuestoRepository.findById(id);
|
||||
|
||||
if (!presupuestoService.canAccessPresupuesto(presupuestoOpt.get(), authentication)) {
|
||||
return ResponseEntity.status(403).build();
|
||||
}
|
||||
|
||||
if (presupuestoOpt.isPresent()) {
|
||||
PresupuestoFormDataDto vm = formDataMapper.toFormData(presupuestoOpt.get());
|
||||
return ResponseEntity.ok(vm);
|
||||
} else {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(path = "/api/save")
|
||||
public ResponseEntity<?> save(
|
||||
@RequestBody Map<String, Object> body,
|
||||
Locale locale, HttpServletRequest request) {
|
||||
|
||||
Presupuesto presupuesto = objectMapper.convertValue(body.get("presupuesto"), Presupuesto.class);
|
||||
Long id = objectMapper.convertValue(body.get("id"), Long.class);
|
||||
String mode = objectMapper.convertValue(body.get("mode"), String.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> serviciosList = (List<Map<String, Object>>) body.getOrDefault("servicios", List.of());
|
||||
Long cliente_id = objectMapper.convertValue(body.get("cliente_id"), Long.class);
|
||||
Map<String, Object> datosMaquetacion = (Map<String, Object>) objectMapper
|
||||
.convertValue(body.get("datosMaquetacion"), Map.class);
|
||||
Map<String, Object> datosMarcapaginas = (Map<String, Object>) objectMapper
|
||||
.convertValue(body.get("datosMarcapaginas"), Map.class);
|
||||
|
||||
Set<ConstraintViolation<Presupuesto>> violations = validator.validate(presupuesto,
|
||||
PresupuestoValidationGroups.All.class);
|
||||
|
||||
if (!violations.isEmpty()) {
|
||||
Map<String, String> errores = new HashMap<>();
|
||||
for (ConstraintViolation<Presupuesto> v : violations) {
|
||||
String campo = v.getPropertyPath().toString();
|
||||
String mensaje = messageSource.getMessage(v.getMessage().replace("{", "").replace("}", ""), null,
|
||||
locale);
|
||||
errores.put(campo, mensaje);
|
||||
}
|
||||
return ResponseEntity.badRequest().body(errores);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
Map<String, Object> saveResult = presupuestoService.guardarPresupuesto(
|
||||
presupuesto,
|
||||
serviciosList,
|
||||
datosMaquetacion,
|
||||
datosMarcapaginas,
|
||||
mode,
|
||||
cliente_id,
|
||||
id,
|
||||
request,
|
||||
locale);
|
||||
|
||||
return ResponseEntity.ok(Map.of("id", saveResult.get("presupuesto_id"),
|
||||
"message", messageSource.getMessage("presupuesto.exito.guardado", null, locale)));
|
||||
} catch (Exception ex) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(Map.of("message",
|
||||
messageSource.getMessage("presupuesto.error.save-internal-error", null, locale),
|
||||
"detail",
|
||||
ex.getClass().getSimpleName() + ": " + (ex.getMessage() != null ? ex.getMessage() : "")));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,118 @@
|
||||
package com.imprimelibros.erp.presupuesto;
|
||||
|
||||
import com.imprimelibros.erp.common.Utils;
|
||||
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.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.*;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
|
||||
@Service
|
||||
public class PresupuestoDatatableService {
|
||||
|
||||
private final MessageSource messageSource;
|
||||
private final PresupuestoRepository repo;
|
||||
|
||||
public PresupuestoDatatableService(MessageSource messageSource, PresupuestoRepository repo) {
|
||||
this.messageSource = messageSource;
|
||||
this.repo = repo;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public DataTablesResponse<Map<String, Object>> datatablePublicos(DataTablesRequest dt, Locale locale) {
|
||||
return commonDataTable(dt, locale, "publico", true);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public DataTablesResponse<Map<String, Object>> datatablePrivados(DataTablesRequest dt, Locale locale) {
|
||||
return commonDataTable(dt, locale, "privado", false);
|
||||
}
|
||||
|
||||
private DataTablesResponse<Map<String, Object>> commonDataTable(DataTablesRequest dt, Locale locale, String origen,
|
||||
boolean publico) {
|
||||
Long count = repo.findAllByOrigen(Presupuesto.Origen.valueOf(origen)).stream().count();
|
||||
|
||||
List<String> orderable = List.of(
|
||||
"id", "titulo", "user.fullName", "tipoEncuadernacion", "tipoCubierta", "tipoImpresion",
|
||||
"selectedTirada", "estado", "totalConIva", "paginas", "pais", "region", "ciudad", "updatedAt");
|
||||
|
||||
return DataTable.of(repo, Presupuesto.class, dt,
|
||||
List.of("")) // búsqueda global solo por campos simples
|
||||
.orderable(orderable)
|
||||
.where((root, query, cb) -> cb.equal(root.get("origen"), Presupuesto.Origen.valueOf(origen)))
|
||||
.onlyAddedColumns()
|
||||
.add("id", Presupuesto::getId)
|
||||
.add("titulo", Presupuesto::getTitulo)
|
||||
.add("tipoEncuadernacion", p -> msg(p.getTipoEncuadernacion().getMessageKey(), locale))
|
||||
.add("tipoCubierta", p -> msg(p.getTipoCubierta().getMessageKey(), locale))
|
||||
.add("tipoImpresion", p -> msg(p.getTipoImpresion().getMessageKey(), locale))
|
||||
.add("selectedTirada", Presupuesto::getSelectedTirada)
|
||||
.add("paginas", p -> n(p.getPaginasColor()) + n(p.getPaginasNegro()))
|
||||
.filter("paginas", (root, q, cb, value) -> {
|
||||
Expression<Integer> sum = cb.sum(
|
||||
cb.coalesce(root.get("paginasColor"), cb.literal(0)),
|
||||
cb.coalesce(root.get("paginasNegro"), cb.literal(0)));
|
||||
Expression<String> asStr = cb.function("CONCAT", String.class, cb.literal(""), sum);
|
||||
return cb.like(asStr, "%" + value.trim() + "%");
|
||||
})
|
||||
.orderable("paginas", (root, q, cb) -> cb.sum(
|
||||
cb.coalesce(root.get("paginasColor"), cb.literal(0)),
|
||||
cb.coalesce(root.get("paginasNegro"), cb.literal(0))))
|
||||
|
||||
.add("estado", p -> msg(p.getEstado().getMessageKey(), locale))
|
||||
.add("totalConIva", p -> Utils.formatCurrency(p.getTotalConIva(), locale))
|
||||
.addIf(publico, "pais", Presupuesto::getPais)
|
||||
.addIf(publico, "region", Presupuesto::getRegion)
|
||||
.addIf(publico, "ciudad", Presupuesto::getCiudad)
|
||||
.add("updatedAt", p -> formatDate(p.getUpdatedAt(), locale))
|
||||
.addIf(!publico, "user", p -> p.getUser() != null ? p.getUser().getFullName() : "")
|
||||
.add("actions", this::generarBotones)
|
||||
.toJson(count);
|
||||
}
|
||||
|
||||
/* ---------- helpers ---------- */
|
||||
|
||||
private String msg(String key, Locale locale) {
|
||||
try {
|
||||
return messageSource.getMessage(key, null, locale);
|
||||
} catch (Exception e) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
private int n(Integer v) {
|
||||
return v == null ? 0 : v;
|
||||
}
|
||||
|
||||
private String formatDate(Instant instant, Locale locale) {
|
||||
if (instant == null)
|
||||
return "";
|
||||
ZoneId zone = (locale != null && locale.getCountry() != null && !locale.getCountry().isEmpty())
|
||||
? TimeZone.getTimeZone(locale.toLanguageTag()).toZoneId()
|
||||
: ZoneId.systemDefault();
|
||||
var df = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm").withZone(zone);
|
||||
return df.format(instant);
|
||||
}
|
||||
|
||||
private String generarBotones(Presupuesto p) {
|
||||
boolean borrador = p.getEstado() == Presupuesto.Estado.borrador;
|
||||
String id = String.valueOf(p.getId());
|
||||
String editBtn = "<a href=\"javascript:void(0);\" data-id=\"" + id + "\" class=\"link-success btn-edit-" +
|
||||
(p.getOrigen().equals(Presupuesto.Origen.publico) ? "anonimo" : "privado") + " fs-15\"><i class=\"ri-" +
|
||||
(p.getOrigen().equals(Presupuesto.Origen.publico) ? "eye" : "pencil") + "-line\"></i></a>";
|
||||
|
||||
String deleteBtn = borrador ? "<a href=\"javascript:void(0);\" data-id=\"" + id
|
||||
+ "\" class=\"link-danger btn-delete-"
|
||||
+ (p.getOrigen().equals(Presupuesto.Origen.publico) ? "anonimo" : "privado")
|
||||
+ " fs-15\"><i class=\"ri-delete-bin-5-line\"></i></a>" : "";
|
||||
|
||||
return "<div class=\"hstack gap-3 flex-wrap\">" + editBtn + deleteBtn + "</div>";
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
package com.imprimelibros.erp.presupuesto;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Repository
|
||||
public interface PresupuestoRepository extends JpaRepository<Presupuesto, Long>, JpaSpecificationExecutor<Presupuesto> {
|
||||
|
||||
Optional<Presupuesto> findFirstBySessionIdAndOrigenAndEstadoInOrderByUpdatedAtDesc(
|
||||
String sessionId,
|
||||
Presupuesto.Origen origen,
|
||||
Collection<Presupuesto.Estado> estados);
|
||||
|
||||
List<Presupuesto> findByOrigenAndEstado(Presupuesto.Origen origen, Presupuesto.Estado estado);
|
||||
|
||||
// Incluye borrados (ignora @Where) usando native
|
||||
@Query(value = "SELECT * FROM presupuesto WHERE id = :id", nativeQuery = true)
|
||||
Optional<Presupuesto> findAnyById(@Param("id") Long id);
|
||||
|
||||
Optional<Presupuesto> findTopBySessionIdAndEstadoOrderByCreatedAtDesc(String sessionId, Presupuesto.Estado estado);
|
||||
|
||||
List<Presupuesto> findAllByOrigen(Presupuesto.Origen origen);
|
||||
}
|
||||
@ -3,7 +3,8 @@ package com.imprimelibros.erp.presupuesto.classes;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.imprimelibros.erp.i18n.TranslationService;
|
||||
import com.imprimelibros.erp.presupuesto.Presupuesto;
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
|
||||
import java.util.Locale;
|
||||
@ -37,8 +38,8 @@ public class PresupuestoFormatter {
|
||||
Object[] args = {
|
||||
|
||||
p.getSelectedTirada(),
|
||||
encuadernacion,
|
||||
tipoImpresion,
|
||||
encuadernacion.toLowerCase(),
|
||||
tipoImpresion.toLowerCase(),
|
||||
(p.getPaginasColorTotal() != null ? p.getPaginasColorTotal() : p.getPaginasColor())
|
||||
+ p.getPaginasNegro(),
|
||||
p.getAncho(), p.getAlto(),
|
||||
|
||||
@ -1,22 +1,42 @@
|
||||
package com.imprimelibros.erp.presupuesto;
|
||||
package com.imprimelibros.erp.presupuesto.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import com.imprimelibros.erp.presupuesto.validation.ConsistentTiradas;
|
||||
import com.imprimelibros.erp.presupuesto.validation.PaginasCosidoGrapado;
|
||||
import com.imprimelibros.erp.presupuesto.validation.Par;
|
||||
import com.imprimelibros.erp.presupuesto.validation.PresupuestoValidationGroups;
|
||||
import com.imprimelibros.erp.presupuesto.validation.Tamanio;
|
||||
|
||||
import com.imprimelibros.erp.common.HtmlStripConverter;
|
||||
import com.imprimelibros.erp.common.jpa.AbstractAuditedEntity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import org.hibernate.annotations.SQLDelete;
|
||||
import org.hibernate.annotations.SQLRestriction;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import com.imprimelibros.erp.users.User;
|
||||
|
||||
@ConsistentTiradas(groups = PresupuestoValidationGroups.DatosGenerales.class)
|
||||
@PaginasCosidoGrapado(groups = PresupuestoValidationGroups.DatosGenerales.class)
|
||||
@Tamanio(groups = PresupuestoValidationGroups.DatosGenerales.class)
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
@Entity
|
||||
@Table(name = "presupuesto")
|
||||
public class Presupuesto implements Cloneable{
|
||||
@Table(name = "presupuesto", indexes = {
|
||||
@Index(name = "idx_presupuesto_origen_estado", columnList = "origen, estado"),
|
||||
@Index(name = "idx_presupuesto_session", columnList = "session_id"),
|
||||
@Index(name = "idx_presupuesto_user", columnList = "user_id"),
|
||||
@Index(name = "idx_presupuesto_deleted", columnList = "deleted"),
|
||||
@Index(name = "idx_presupuesto_geo", columnList = "pais, region, ciudad")
|
||||
})
|
||||
@SQLDelete(sql = "UPDATE presupuesto SET deleted = 1, deleted_at = NOW(3) WHERE id = ?")
|
||||
@SQLRestriction("deleted = 0")
|
||||
public class Presupuesto extends AbstractAuditedEntity implements Cloneable {
|
||||
|
||||
public enum TipoEncuadernacion {
|
||||
fresado("presupuesto.fresado"),
|
||||
@ -69,6 +89,38 @@ public class Presupuesto implements Cloneable{
|
||||
}
|
||||
}
|
||||
|
||||
public enum Origen {
|
||||
publico, privado
|
||||
}
|
||||
|
||||
public enum Estado {
|
||||
borrador("presupuesto.estado.borrador"),
|
||||
aceptado("presupuesto.estado.aceptado"),
|
||||
modificado("presupuesto.estado.modificado");
|
||||
|
||||
private final String messageKey;
|
||||
Estado(String messageKey) {
|
||||
this.messageKey = messageKey;
|
||||
}
|
||||
public String getMessageKey() {
|
||||
return messageKey;
|
||||
}
|
||||
}
|
||||
|
||||
public enum Entrega{
|
||||
peninsula("presupuesto.entrega.peninsula"),
|
||||
canarias("presupuesto.entrega.canarias"),
|
||||
paises_ue("presupuesto.entrega.paises-ue");
|
||||
|
||||
private final String messageKey;
|
||||
Entrega(String messageKey) {
|
||||
this.messageKey = messageKey;
|
||||
}
|
||||
public String getMessageKey() {
|
||||
return messageKey;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Presupuesto clone() {
|
||||
try {
|
||||
@ -78,8 +130,100 @@ public class Presupuesto implements Cloneable{
|
||||
}
|
||||
}
|
||||
|
||||
// ====== NUEVOS: Origen/Estado/Usuario/Session/Geo/IP/Totales/JSONs ======
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "origen", nullable = false)
|
||||
private Origen origen = Origen.publico;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "estado", nullable = false)
|
||||
private Estado estado = Estado.borrador;
|
||||
|
||||
// Usuario autenticado (nullable en público)
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "user_id")
|
||||
private User user;
|
||||
|
||||
// Continuidad en público
|
||||
@Column(name = "session_id", length = 64)
|
||||
private String sessionId;
|
||||
|
||||
@Column(name = "visitor_id", length = 64)
|
||||
private String visitorId;
|
||||
|
||||
// IP anonimizada / truncada + geolocalización resumida (para estadísticas)
|
||||
@Column(name = "ip_hash", length = 88) // SHA-256 base64 ≈ 44 chars; dejamos margen
|
||||
private String ipHash;
|
||||
|
||||
@Column(name = "ip_trunc", length = 64)
|
||||
private String ipTrunc; // p.ej. "192.168.0.0" o "2a02:xxxx::"
|
||||
|
||||
@Column(name = "pais", length = 64)
|
||||
private String pais;
|
||||
|
||||
@Column(name = "region", length = 128)
|
||||
private String region;
|
||||
|
||||
@Column(name = "ciudad", length = 128)
|
||||
private String ciudad;
|
||||
|
||||
// Totales de la tirada seleccionada y del presupuesto
|
||||
@Column(name = "precio_unitario", precision = 12, scale = 4)
|
||||
private BigDecimal precioUnitario;
|
||||
|
||||
@Column(name = "precio_total_tirada", precision = 12, scale = 2)
|
||||
private BigDecimal precioTotalTirada;
|
||||
|
||||
@Column(name = "servicios_total", precision = 12, scale = 2)
|
||||
private BigDecimal serviciosTotal;
|
||||
|
||||
@Column(name = "base_imponible", precision = 12, scale = 2)
|
||||
private BigDecimal baseImponible;
|
||||
|
||||
@Column(name = "iva_reducido")
|
||||
private Boolean ivaReducido;
|
||||
|
||||
@Column(name = "entrega_tipo")
|
||||
@Enumerated(EnumType.STRING)
|
||||
private Entrega entregaTipo;
|
||||
|
||||
@Column(name = "iva_importe_4", precision = 12, scale = 2)
|
||||
private BigDecimal ivaImporte4;
|
||||
|
||||
@Column(name = "iva_importe_21", precision = 12, scale = 2)
|
||||
private BigDecimal ivaImporte21;
|
||||
|
||||
@Column(name = "total_con_iva", precision = 12, scale = 2)
|
||||
private BigDecimal totalConIva;
|
||||
|
||||
// JSONs de apoyo (todas las tiradas, servicios, bloques de
|
||||
// maquetación/marcapáginas y snapshot)
|
||||
@Lob
|
||||
@Column(name = "precios_por_tirada_json", columnDefinition = "json")
|
||||
private String preciosPorTiradaJson; // [{tirada, precio_unitario, precio_total_tirada}, ...]
|
||||
|
||||
@Lob
|
||||
@Column(name = "servicios_json", columnDefinition = "json")
|
||||
private String serviciosJson;
|
||||
|
||||
@Lob
|
||||
@Column(name = "datos_maquetacion_json", columnDefinition = "json")
|
||||
private String datosMaquetacionJson;
|
||||
|
||||
@Lob
|
||||
@Column(name = "datos_marcapaginas_json", columnDefinition = "json")
|
||||
private String datosMarcapaginasJson;
|
||||
|
||||
@Lob
|
||||
@Column(name = "pricing_snapshot", columnDefinition = "json")
|
||||
private String pricingSnapshotJson;
|
||||
|
||||
// ====== TUS CAMPOS ORIGINALES ======
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "id")
|
||||
private Long id;
|
||||
|
||||
@NotNull(message = "{presupuesto.errores.tipo-encuadernacion}", groups = PresupuestoValidationGroups.DatosGenerales.class)
|
||||
@ -192,39 +336,270 @@ public class Presupuesto implements Cloneable{
|
||||
|
||||
@NotNull(message = "{presupuesto.errores.acabado-cubierta}", groups = PresupuestoValidationGroups.Cubierta.class)
|
||||
@Column(name = "acabado")
|
||||
private Integer acabado = 1;
|
||||
private Integer acabado = 1;
|
||||
|
||||
@Column(name = "sobrecubierta")
|
||||
private Boolean sobrecubierta = false;
|
||||
|
||||
@Column(name = "papel_sobrecubierta_id")
|
||||
private Integer papelSobrecubiertaId = 2;
|
||||
|
||||
@Column(name = "gramaje_sobrecubierta")
|
||||
private Integer gramajeSobrecubierta = 170;
|
||||
|
||||
@Column(name = "tamanio_solapas_sobrecubierta")
|
||||
private Integer tamanioSolapasSobrecubierta = 80;
|
||||
|
||||
@Column(name = "acabado_sobrecubierta")
|
||||
private Integer acabadoSobrecubierta = 0; // 0: sin acabado,
|
||||
private Integer acabadoSobrecubierta = 0;
|
||||
|
||||
@Column(name = "faja")
|
||||
private Boolean faja = false;
|
||||
|
||||
@Column(name = "papel_faja_id")
|
||||
private Integer papelFajaId = 2;
|
||||
|
||||
@Column(name = "gramaje_faja")
|
||||
private Integer gramajeFaja = 170;
|
||||
|
||||
@Column(name = "tamanio_solapas_faja")
|
||||
private Integer tamanioSolapasFaja = 80;
|
||||
|
||||
@Column(name = "acabado_faja")
|
||||
private Integer acabadoFaja = 0; // 0: sin acabado
|
||||
private Integer acabadoFaja = 0;
|
||||
|
||||
@Column(name = "alto_faja")
|
||||
private Integer altoFaja = 0;
|
||||
|
||||
@Column(name = "presupuesto_maquetacion")
|
||||
private Boolean presupuestoMaquetacion = false;
|
||||
@Column(name = "presupuesto_maquetacion_data")
|
||||
private String presupuestoMaquetacionData;
|
||||
// ====== MÉTODOS AUX ======
|
||||
|
||||
public String resumenPresupuesto() {
|
||||
return String.format("%s - %s - %dx%d mm - %d Páginas (N:%d C:%d) - Tira:%d",
|
||||
this.titulo,
|
||||
this.tipoEncuadernacion,
|
||||
this.ancho,
|
||||
this.alto,
|
||||
(this.paginasNegro != null ? this.paginasNegro : 0)
|
||||
+ (this.paginasColorTotal != null ? this.paginasColorTotal : 0),
|
||||
this.paginasNegro != null ? this.paginasNegro : 0,
|
||||
this.paginasColorTotal != null ? this.paginasColorTotal : 0,
|
||||
this.selectedTirada != null ? this.selectedTirada : 0);
|
||||
}
|
||||
|
||||
public Integer[] getTiradas() {
|
||||
return new Integer[] { tirada1, tirada2, tirada3, tirada4 };
|
||||
}
|
||||
|
||||
// ====== GETTERS/SETTERS (incluye nuevos y existentes) ======
|
||||
|
||||
public Origen getOrigen() {
|
||||
return origen;
|
||||
}
|
||||
|
||||
public void setOrigen(Origen origen) {
|
||||
this.origen = origen;
|
||||
}
|
||||
|
||||
public Estado getEstado() {
|
||||
return estado;
|
||||
}
|
||||
|
||||
public void setEstado(Estado estado) {
|
||||
this.estado = estado;
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public void setUser(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
public String getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public void setSessionId(String sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
public String getVisitorId() {
|
||||
return visitorId;
|
||||
}
|
||||
|
||||
public void setVisitorId(String visitorId) {
|
||||
this.visitorId = visitorId;
|
||||
}
|
||||
|
||||
public String getIpHash() {
|
||||
return ipHash;
|
||||
}
|
||||
|
||||
public void setIpHash(String ipHash) {
|
||||
this.ipHash = ipHash;
|
||||
}
|
||||
|
||||
public String getIpTrunc() {
|
||||
return ipTrunc;
|
||||
}
|
||||
|
||||
public void setIpTrunc(String ipTrunc) {
|
||||
this.ipTrunc = ipTrunc;
|
||||
}
|
||||
|
||||
public String getPais() {
|
||||
return pais;
|
||||
}
|
||||
|
||||
public void setPais(String pais) {
|
||||
this.pais = pais;
|
||||
}
|
||||
|
||||
public String getRegion() {
|
||||
return region;
|
||||
}
|
||||
|
||||
public void setRegion(String region) {
|
||||
this.region = region;
|
||||
}
|
||||
|
||||
public String getCiudad() {
|
||||
return ciudad;
|
||||
}
|
||||
|
||||
public void setCiudad(String ciudad) {
|
||||
this.ciudad = ciudad;
|
||||
}
|
||||
|
||||
public BigDecimal getPrecioUnitario() {
|
||||
return precioUnitario;
|
||||
}
|
||||
|
||||
public void setPrecioUnitario(BigDecimal precioUnitario) {
|
||||
this.precioUnitario = precioUnitario;
|
||||
}
|
||||
|
||||
public BigDecimal getPrecioTotalTirada() {
|
||||
return precioTotalTirada;
|
||||
}
|
||||
|
||||
public void setPrecioTotalTirada(BigDecimal precioTotalTirada) {
|
||||
this.precioTotalTirada = precioTotalTirada;
|
||||
}
|
||||
|
||||
public BigDecimal getServiciosTotal() {
|
||||
return serviciosTotal;
|
||||
}
|
||||
|
||||
public void setServiciosTotal(BigDecimal serviciosTotal) {
|
||||
this.serviciosTotal = serviciosTotal;
|
||||
}
|
||||
|
||||
public BigDecimal getBaseImponible() {
|
||||
return baseImponible;
|
||||
}
|
||||
|
||||
public void setBaseImponible(BigDecimal baseImponible) {
|
||||
this.baseImponible = baseImponible;
|
||||
}
|
||||
|
||||
public Boolean getIvaReducido() {
|
||||
return ivaReducido;
|
||||
}
|
||||
|
||||
public void setIvaReducido(Boolean ivaReducido) {
|
||||
this.ivaReducido = ivaReducido;
|
||||
}
|
||||
|
||||
public Entrega getEntregaTipo() {
|
||||
return entregaTipo;
|
||||
}
|
||||
|
||||
public void setEntregaTipo(Entrega entregaTipo) {
|
||||
this.entregaTipo = entregaTipo;
|
||||
}
|
||||
|
||||
public BigDecimal getIvaImporte4() {
|
||||
return ivaImporte4;
|
||||
}
|
||||
|
||||
public void setIvaImporte4(BigDecimal ivaImporte4) {
|
||||
this.ivaImporte4 = ivaImporte4;
|
||||
}
|
||||
|
||||
public BigDecimal getIvaImporte21() {
|
||||
return ivaImporte21;
|
||||
}
|
||||
|
||||
public void setIvaImporte21(BigDecimal ivaImporte21) {
|
||||
this.ivaImporte21 = ivaImporte21;
|
||||
}
|
||||
|
||||
public BigDecimal getTotalConIva() {
|
||||
return totalConIva;
|
||||
}
|
||||
|
||||
public void setTotalConIva(BigDecimal totalConIva) {
|
||||
this.totalConIva = totalConIva;
|
||||
}
|
||||
|
||||
public String getPreciosPorTiradaJson() {
|
||||
return preciosPorTiradaJson;
|
||||
}
|
||||
|
||||
public void setPreciosPorTiradaJson(String preciosPorTiradaJson) {
|
||||
this.preciosPorTiradaJson = preciosPorTiradaJson;
|
||||
}
|
||||
|
||||
public String getServiciosJson() {
|
||||
return serviciosJson;
|
||||
}
|
||||
|
||||
public void setServiciosJson(String serviciosJson) {
|
||||
this.serviciosJson = serviciosJson;
|
||||
}
|
||||
|
||||
public String getDatosMaquetacionJson() {
|
||||
return datosMaquetacionJson;
|
||||
}
|
||||
|
||||
public void setDatosMaquetacionJson(String datosMaquetacionJson) {
|
||||
this.datosMaquetacionJson = datosMaquetacionJson;
|
||||
}
|
||||
|
||||
public String getDatosMarcapaginasJson() {
|
||||
return datosMarcapaginasJson;
|
||||
}
|
||||
|
||||
public void setDatosMarcapaginasJson(String datosMarcapaginasJson) {
|
||||
this.datosMarcapaginasJson = datosMarcapaginasJson;
|
||||
}
|
||||
|
||||
public String getPricingSnapshotJson() {
|
||||
return pricingSnapshotJson;
|
||||
}
|
||||
|
||||
public void setPricingSnapshotJson(String pricingSnapshotJson) {
|
||||
this.pricingSnapshotJson = pricingSnapshotJson;
|
||||
}
|
||||
|
||||
public TipoEncuadernacion getTipoEncuadernacion() {
|
||||
return tipoEncuadernacion;
|
||||
}
|
||||
|
||||
public void setTipoEncuadernacion(TipoEncuadernacion tipoEncuadernacion) {
|
||||
this.tipoEncuadernacion = tipoEncuadernacion;
|
||||
}
|
||||
|
||||
public String getTitulo() {
|
||||
return titulo;
|
||||
}
|
||||
|
||||
public void setTitulo(String titulo) {
|
||||
this.titulo = titulo;
|
||||
}
|
||||
|
||||
// Getters y Setters
|
||||
public String getAutor() {
|
||||
return autor;
|
||||
}
|
||||
@ -269,14 +644,18 @@ public class Presupuesto implements Cloneable{
|
||||
return tirada4;
|
||||
}
|
||||
|
||||
public Integer[] getTiradas() {
|
||||
return new Integer[] { tirada1, tirada2, tirada3, tirada4 };
|
||||
}
|
||||
|
||||
public void setTirada4(Integer tirada4) {
|
||||
this.tirada4 = tirada4;
|
||||
}
|
||||
|
||||
public Integer getSelectedTirada() {
|
||||
return selectedTirada;
|
||||
}
|
||||
|
||||
public void setSelectedTirada(Integer selectedTirada) {
|
||||
this.selectedTirada = selectedTirada;
|
||||
}
|
||||
|
||||
public Integer getAncho() {
|
||||
return ancho;
|
||||
}
|
||||
@ -301,22 +680,6 @@ public class Presupuesto implements Cloneable{
|
||||
this.formatoPersonalizado = formatoPersonalizado;
|
||||
}
|
||||
|
||||
public String getTitulo() {
|
||||
return titulo;
|
||||
}
|
||||
|
||||
public void setTitulo(String titulo) {
|
||||
this.titulo = titulo;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Integer getPaginasNegro() {
|
||||
return paginasNegro;
|
||||
}
|
||||
@ -333,14 +696,6 @@ public class Presupuesto implements Cloneable{
|
||||
this.paginasColor = paginasColor;
|
||||
}
|
||||
|
||||
public TipoEncuadernacion getTipoEncuadernacion() {
|
||||
return tipoEncuadernacion;
|
||||
}
|
||||
|
||||
public void setTipoEncuadernacion(TipoEncuadernacion tipoEncuadernacion) {
|
||||
this.tipoEncuadernacion = tipoEncuadernacion;
|
||||
}
|
||||
|
||||
public String getPosicionPaginasColor() {
|
||||
return posicionPaginasColor;
|
||||
}
|
||||
@ -557,36 +912,12 @@ public class Presupuesto implements Cloneable{
|
||||
this.altoFaja = altoFaja;
|
||||
}
|
||||
|
||||
public Integer getSelectedTirada() {
|
||||
return selectedTirada;
|
||||
public Long getId(){
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setSelectedTirada(Integer selectedTirada) {
|
||||
this.selectedTirada = selectedTirada;
|
||||
}
|
||||
|
||||
public Boolean getPresupuestoMaquetacion() {
|
||||
return presupuestoMaquetacion;
|
||||
}
|
||||
public void setPresupuestoMaquetacion(Boolean presupuestoMaquetacion) {
|
||||
this.presupuestoMaquetacion = presupuestoMaquetacion;
|
||||
}
|
||||
public String getPresupuestoMaquetacionData() {
|
||||
return presupuestoMaquetacionData;
|
||||
}
|
||||
public void setPresupuestoMaquetacionData(String presupuestoMaquetacionData) {
|
||||
this.presupuestoMaquetacionData = presupuestoMaquetacionData;
|
||||
}
|
||||
|
||||
public String resumenPresupuesto() {
|
||||
return String.format("%s - %s - %dx%d mm - %d Páginas (N:%d C:%d) - Tira:%d",
|
||||
this.titulo,
|
||||
this.tipoEncuadernacion,
|
||||
this.ancho,
|
||||
this.alto,
|
||||
this.paginasNegro + this.paginasColorTotal,
|
||||
this.paginasNegro,
|
||||
this.paginasColorTotal,
|
||||
this.selectedTirada != null ? this.selectedTirada : 0);
|
||||
public void setId(Long id){
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package com.imprimelibros.erp.presupuesto.geo;
|
||||
|
||||
import com.imprimelibros.erp.presupuesto.GeoIpService;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class CompositeGeoIpService implements GeoIpService {
|
||||
|
||||
private final List<GeoIpService> delegates;
|
||||
|
||||
public CompositeGeoIpService(List<GeoIpService> delegates) {
|
||||
this.delegates = delegates;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<GeoData> lookup(String ip) {
|
||||
for (GeoIpService d : delegates) {
|
||||
try {
|
||||
Optional<GeoData> r = d.lookup(ip);
|
||||
if (r.isPresent()) return r;
|
||||
} catch (Exception ignore) { /* tolerante */ }
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
package com.imprimelibros.erp.presupuesto.geo;
|
||||
|
||||
import com.imprimelibros.erp.presupuesto.GeoIpService;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
public class GeoIpConfig {
|
||||
|
||||
// MaxMind (local). Activar con: geoip.maxmind.enabled=true
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "geoip.maxmind", name = "enabled", havingValue = "true")
|
||||
public GeoIpService maxMindGeoIpService(ResourceLoader loader,
|
||||
GeoIpProperties props) {
|
||||
String path = props.getMaxmind().dbClasspathLocation;
|
||||
return new MaxMindGeoIpService(loader, path);
|
||||
}
|
||||
|
||||
// HTTP. Activar con: geoip.http.enabled=true
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "geoip.http", name = "enabled", havingValue = "true")
|
||||
public GeoIpService httpGeoIpService(GeoIpProperties props) {
|
||||
RestTemplate rt = new RestTemplate();
|
||||
return new HttpGeoIpService(rt, props.getHttp().endpointTemplate);
|
||||
}
|
||||
|
||||
// Composite. Activo si cualquiera de los anteriores lo está: geoip.enabled=true
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "geoip", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
@Primary
|
||||
public GeoIpService compositeGeoIpService(List<GeoIpService> delegates) {
|
||||
// Si no hay ninguno, lista vacía → Composite devuelve empty y no rompe nada
|
||||
return new CompositeGeoIpService(new ArrayList<>(delegates));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GeoIpProperties geoIpProperties() {
|
||||
return new GeoIpProperties();
|
||||
}
|
||||
|
||||
// --- Props holder simple ---
|
||||
public static class GeoIpProperties {
|
||||
private MaxMindProps maxmind = new MaxMindProps();
|
||||
private HttpProps http = new HttpProps();
|
||||
|
||||
public MaxMindProps getMaxmind() { return maxmind; }
|
||||
public HttpProps getHttp() { return http; }
|
||||
|
||||
public static class MaxMindProps {
|
||||
public boolean enabled = false;
|
||||
public String dbClasspathLocation = "classpath:geoip/GeoLite2-City.mmdb";
|
||||
}
|
||||
public static class HttpProps {
|
||||
public boolean enabled = false;
|
||||
// {ip} será reemplazado por la IP
|
||||
public String endpointTemplate = "https://ipapi.co/{ip}/json/";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
package com.imprimelibros.erp.presupuesto.geo;
|
||||
|
||||
import com.imprimelibros.erp.presupuesto.GeoIpService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public class HttpGeoIpService implements GeoIpService {
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
private final String endpointTemplate; // p.ej. "https://ipapi.co/{ip}/json/"
|
||||
|
||||
public HttpGeoIpService(RestTemplate restTemplate, String endpointTemplate) {
|
||||
this.restTemplate = restTemplate;
|
||||
this.endpointTemplate = endpointTemplate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<GeoData> lookup(String ip) {
|
||||
try {
|
||||
if (ip == null || ip.isBlank()) return Optional.empty();
|
||||
if (isPrivateIp(ip)) return Optional.empty();
|
||||
|
||||
String url = endpointTemplate.replace("{ip}", ip);
|
||||
ResponseEntity<Map> resp = restTemplate.getForEntity(url, Map.class);
|
||||
if (!resp.getStatusCode().is2xxSuccessful() || resp.getBody() == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Map body = resp.getBody();
|
||||
|
||||
// Campos más comunes según proveedor
|
||||
String pais = firstNonBlank(
|
||||
(String) body.get("country_name"),
|
||||
(String) body.get("country")
|
||||
);
|
||||
String region = firstNonBlank(
|
||||
(String) body.get("region"),
|
||||
(String) body.get("regionName"),
|
||||
(String) body.get("state")
|
||||
);
|
||||
String ciudad = firstNonBlank(
|
||||
(String) body.get("city")
|
||||
);
|
||||
|
||||
if (isBlank(pais) && isBlank(region) && isBlank(ciudad)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(new GeoData(pais, region, ciudad));
|
||||
} catch (Exception e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isBlank(String s) { return s == null || s.isBlank(); }
|
||||
private static String firstNonBlank(String... vals) {
|
||||
for (String v : vals) if (!isBlank(v)) return v;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isPrivateIp(String ip) {
|
||||
return ip.startsWith("10.") ||
|
||||
ip.startsWith("192.168.") ||
|
||||
ip.matches("^172\\.(1[6-9]|2\\d|3[0-1])\\..*") ||
|
||||
ip.equals("127.0.0.1") ||
|
||||
ip.startsWith("169.254.") ||
|
||||
ip.equals("::1") ||
|
||||
ip.startsWith("fe80:") ||
|
||||
ip.startsWith("fc00:") || ip.startsWith("fd00:");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
package com.imprimelibros.erp.presupuesto.geo;
|
||||
|
||||
import com.imprimelibros.erp.presupuesto.GeoIpService;
|
||||
import com.maxmind.geoip2.DatabaseReader;
|
||||
import com.maxmind.geoip2.exception.GeoIp2Exception;
|
||||
import com.maxmind.geoip2.model.CityResponse;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.util.Optional;
|
||||
|
||||
public class MaxMindGeoIpService implements GeoIpService, AutoCloseable {
|
||||
|
||||
private final DatabaseReader dbReader;
|
||||
|
||||
public MaxMindGeoIpService(ResourceLoader resourceLoader, String dbClasspathLocation) {
|
||||
try {
|
||||
Resource resource = resourceLoader.getResource(dbClasspathLocation);
|
||||
this.dbReader = new DatabaseReader.Builder(resource.getInputStream()).build();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("No se pudo cargar la base de MaxMind desde: " + dbClasspathLocation, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<GeoData> lookup(String ip) {
|
||||
try {
|
||||
if (ip == null || ip.isBlank()) return Optional.empty();
|
||||
// Ignora IPs privadas habituales
|
||||
if (isPrivateIp(ip)) return Optional.empty();
|
||||
|
||||
InetAddress addr = InetAddress.getByName(ip);
|
||||
CityResponse resp = dbReader.city(addr);
|
||||
|
||||
String pais = safe(resp.getCountry() != null ? resp.getCountry().getNames().get("es") : null,
|
||||
resp.getCountry() != null ? resp.getCountry().getName() : null);
|
||||
String region = safe(resp.getMostSpecificSubdivision() != null ? resp.getMostSpecificSubdivision().getNames().get("es") : null,
|
||||
resp.getMostSpecificSubdivision() != null ? resp.getMostSpecificSubdivision().getName() : null);
|
||||
String ciudad = safe(resp.getCity() != null ? resp.getCity().getNames().get("es") : null,
|
||||
resp.getCity() != null ? resp.getCity().getName() : null);
|
||||
|
||||
if (isAllBlank(pais, region, ciudad)) return Optional.empty();
|
||||
return Optional.of(new GeoData(pais, region, ciudad));
|
||||
} catch (IOException | GeoIp2Exception e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private static String safe(String... candidates) {
|
||||
for (String c : candidates) if (c != null && !c.isBlank()) return c;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isAllBlank(String... vals) {
|
||||
for (String v : vals) if (v != null && !v.isBlank()) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean isPrivateIp(String ip) {
|
||||
// Simplificado: rangos privados IPv4, y link-local/loopback. IPv6 simplificado.
|
||||
return ip.startsWith("10.") ||
|
||||
ip.startsWith("192.168.") ||
|
||||
ip.matches("^172\\.(1[6-9]|2\\d|3[0-1])\\..*") ||
|
||||
ip.equals("127.0.0.1") ||
|
||||
ip.startsWith("169.254.") ||
|
||||
ip.equals("::1") ||
|
||||
ip.startsWith("fe80:") ||
|
||||
ip.startsWith("fc00:") || ip.startsWith("fd00:");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
if (dbReader != null) dbReader.close();
|
||||
}
|
||||
}
|
||||
@ -7,11 +7,29 @@ import jakarta.persistence.*;
|
||||
public class MaquetacionMatrices {
|
||||
|
||||
public enum Formato{
|
||||
A5, _17x24_, A4
|
||||
A5, _17x24_, A4;
|
||||
private final String label;
|
||||
Formato() {
|
||||
this.label = this.name().indexOf('_') > -1 ? this.name().replace("_", "") + " mm" : this.name();
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
|
||||
public enum FontSize{
|
||||
small, medium, big
|
||||
small("presupuesto.maquetacion.cuerpo-texto-pequeño"),
|
||||
medium("presupuesto.maquetacion.cuerpo-texto-medio"),
|
||||
big("presupuesto.maquetacion.cuerpo-texto-grande");
|
||||
|
||||
private final String messageKey;
|
||||
FontSize(String messageKey) {
|
||||
this.messageKey = messageKey;
|
||||
}
|
||||
public String getMessageKey() {
|
||||
return messageKey;
|
||||
}
|
||||
}
|
||||
|
||||
@Id
|
||||
|
||||
@ -10,23 +10,61 @@ import jakarta.validation.constraints.Min;
|
||||
public class Marcapaginas {
|
||||
|
||||
public enum Acabado{
|
||||
ninguno,
|
||||
plastificado_brillo_1c,
|
||||
plastificado_brillo_2c,
|
||||
plastificado_mate_1c,
|
||||
plastificado_mate_2c
|
||||
ninguno("presupuesto.marcapaginas.acabado.ninguno"),
|
||||
plastificado_brillo_1c("presupuesto.marcapaginas.acabado.plastificado-brillo-1c"),
|
||||
plastificado_brillo_2c("presupuesto.marcapaginas.acabado.plastificado-brillo-2c"),
|
||||
plastificado_mate_1c("presupuesto.marcapaginas.acabado.plastificado-mate-1c"),
|
||||
plastificado_mate_2c("presupuesto.marcapaginas.acabado.plastificado-mate-2c");
|
||||
|
||||
private final String messageKey;
|
||||
|
||||
Acabado(String messageKey) {
|
||||
this.messageKey = messageKey;
|
||||
}
|
||||
|
||||
public String getMessageKey() {
|
||||
return messageKey;
|
||||
}
|
||||
};
|
||||
|
||||
public enum Tamanios{
|
||||
_50x140_, _50x170_, _50x210_
|
||||
_50x140_,
|
||||
_50x170_,
|
||||
_50x210_;
|
||||
|
||||
private final String label;
|
||||
Tamanios() {
|
||||
this.label = this.name().replace("_", "") + " mm";
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
};
|
||||
|
||||
public enum Papeles{
|
||||
cartulina_grafica, estucado_mate
|
||||
cartulina_grafica,
|
||||
estucado_mate;
|
||||
|
||||
private final String messageKey;
|
||||
Papeles() {
|
||||
this.messageKey = "presupuesto.marcapaginas.papel." + this.name().replace("_", "-");
|
||||
}
|
||||
public String getMessageKey() {
|
||||
return messageKey;
|
||||
}
|
||||
};
|
||||
|
||||
public enum Caras_Impresion{
|
||||
una_cara, dos_caras
|
||||
una_cara("presupuesto.marcapaginas.caras-impresion-1"),
|
||||
dos_caras("presupuesto.marcapaginas.caras-impresion-2");
|
||||
private final String messageKey;
|
||||
Caras_Impresion(String messageKey) {
|
||||
this.messageKey = messageKey;
|
||||
}
|
||||
public String getMessageKey() {
|
||||
return messageKey;
|
||||
}
|
||||
};
|
||||
|
||||
@Id
|
||||
|
||||
@ -0,0 +1,264 @@
|
||||
package com.imprimelibros.erp.presupuesto.service;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class PresupuestoFormDataMapper {
|
||||
|
||||
public class PresupuestoFormDataDto {
|
||||
|
||||
public DatosGenerales datosGenerales = new DatosGenerales();
|
||||
public Interior interior = new Interior();
|
||||
public Cubierta cubierta = new Cubierta();
|
||||
public Servicios servicios = new Servicios();
|
||||
public Integer selectedTirada;
|
||||
|
||||
// ===== Datos Generales =====
|
||||
public static class DatosGenerales {
|
||||
public String titulo = "";
|
||||
public String autor = "";
|
||||
public String isbn = "";
|
||||
public String tirada1 = "";
|
||||
public String tirada2 = "";
|
||||
public String tirada3 = "";
|
||||
public String tirada4 = "";
|
||||
public Integer ancho;
|
||||
public Integer alto;
|
||||
public boolean formatoPersonalizado = false;
|
||||
public String paginasNegro = "";
|
||||
public String paginasColor = "";
|
||||
public String posicionPaginasColor = "";
|
||||
public String tipoEncuadernacion = "fresado"; // enum name
|
||||
public String entregaTipo = "peninsula"; // enum name
|
||||
public boolean ivaReducido = true;
|
||||
}
|
||||
|
||||
// ===== Interior =====
|
||||
public static class Interior {
|
||||
public String tipoImpresion = "negro"; // enum name
|
||||
public Integer papelInteriorId = 3;
|
||||
public Integer gramajeInterior = 80;
|
||||
}
|
||||
|
||||
// ===== Cubierta =====
|
||||
public static class Cubierta {
|
||||
public String tipoCubierta = "tapaBlanda"; // enum name
|
||||
public int solapasCubierta = 0; // 0/1 para tu UI
|
||||
public String tamanioSolapasCubierta = "80";
|
||||
public Integer cubiertaCaras = 2;
|
||||
|
||||
public Integer papelGuardasId = 3;
|
||||
public Integer gramajeGuardas = 170;
|
||||
public Integer guardasImpresas = 0;
|
||||
public String cabezada = "WHI";
|
||||
|
||||
public Integer papelCubiertaId = 3;
|
||||
public Integer gramajeCubierta = 170;
|
||||
public Integer acabado = 1;
|
||||
|
||||
public SobreCubierta sobrecubierta = new SobreCubierta();
|
||||
public Faja faja = new Faja();
|
||||
|
||||
public static class SobreCubierta {
|
||||
public boolean activo = false;
|
||||
public Integer papelSobrecubiertaId = 2;
|
||||
public Integer gramajeSobrecubierta = 170;
|
||||
public Integer tamanioSolapasSobrecubierta = 80;
|
||||
public Integer acabado = 0;
|
||||
}
|
||||
|
||||
public static class Faja {
|
||||
public boolean activo = false;
|
||||
public Integer papelFajaId = 2;
|
||||
public Integer gramajeFaja = 170;
|
||||
public Integer alto = 50;
|
||||
public Integer tamanioSolapasFaja = 80;
|
||||
public Integer acabado = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Servicios / Extras =====
|
||||
public static class Servicios {
|
||||
public List<DatosServicios> servicios = new ArrayList<DatosServicios>();
|
||||
public DatosMarcapaginas datosMarcapaginas = new DatosMarcapaginas();
|
||||
public DatosMaquetacion datosMaquetacion = new DatosMaquetacion();
|
||||
|
||||
public static class DatosServicios {
|
||||
public String id;
|
||||
public String label;
|
||||
public Integer units;
|
||||
public Double price;
|
||||
}
|
||||
|
||||
public static class DatosMarcapaginas {
|
||||
public Integer marcapaginas_tirada = 100;
|
||||
public String tamanio = "_50x140_";
|
||||
public String carasImpresion = "una_cara";
|
||||
public String papel = "cartulina_grafica";
|
||||
public Integer gramaje = 300;
|
||||
public String acabado = "ninguno";
|
||||
public Resultado resultado = new Resultado();
|
||||
}
|
||||
|
||||
public static class DatosMaquetacion {
|
||||
public Integer num_caracteres = 200000;
|
||||
public String formato_maquetacion = "A5";
|
||||
public String cuerpo_texto = "medium";
|
||||
public Integer num_columnas = 1;
|
||||
public Integer num_tablas = 0;
|
||||
public Integer num_fotos = 0;
|
||||
public boolean correccion_ortotipografica = false;
|
||||
public boolean texto_mecanografiado = false;
|
||||
public boolean disenio_portada = false;
|
||||
public boolean epub = false;
|
||||
public Resultado resultado = new Resultado();
|
||||
}
|
||||
|
||||
public static class Resultado {
|
||||
public Integer num_paginas_estimadas = 0; // solo para maquetación
|
||||
public Integer precio_pagina_estimado = 0;
|
||||
public Number precio_unitario = 0; // solo para marcapáginas
|
||||
public Number precio = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final ObjectMapper om = new ObjectMapper();
|
||||
|
||||
public PresupuestoFormDataDto toFormData(Presupuesto p) {
|
||||
|
||||
PresupuestoFormDataDto vm = new PresupuestoFormDataDto();
|
||||
|
||||
// ===== Datos Generales
|
||||
vm.datosGenerales.titulo = nz(p.getTitulo(), "");
|
||||
vm.datosGenerales.autor = nz(p.getAutor(), "");
|
||||
vm.datosGenerales.isbn = nz(p.getIsbn(), "");
|
||||
|
||||
vm.datosGenerales.tirada1 = nzStr(p.getTirada1());
|
||||
vm.datosGenerales.tirada2 = nzStr(p.getTirada2());
|
||||
vm.datosGenerales.tirada3 = nzStr(p.getTirada3());
|
||||
vm.datosGenerales.tirada4 = nzStr(p.getTirada4());
|
||||
|
||||
vm.datosGenerales.ancho = p.getAncho();
|
||||
vm.datosGenerales.alto = p.getAlto();
|
||||
vm.datosGenerales.formatoPersonalizado = Boolean.TRUE.equals(p.getFormatoPersonalizado());
|
||||
|
||||
vm.datosGenerales.paginasNegro = nzStr(p.getPaginasNegro());
|
||||
vm.datosGenerales.paginasColor = nzStr(p.getPaginasColor());
|
||||
vm.datosGenerales.posicionPaginasColor = nz(p.getPosicionPaginasColor(), "");
|
||||
|
||||
vm.datosGenerales.tipoEncuadernacion = enumName(p.getTipoEncuadernacion(), "fresado");
|
||||
|
||||
vm.datosGenerales.entregaTipo = enumName(p.getEntregaTipo(), "peninsula");
|
||||
vm.datosGenerales.ivaReducido = Boolean.TRUE.equals(p.getIvaReducido());
|
||||
|
||||
// ===== Interior
|
||||
vm.interior.tipoImpresion = enumName(p.getTipoImpresion(), "negro");
|
||||
vm.interior.papelInteriorId = nz(p.getPapelInteriorId(), 3);
|
||||
vm.interior.gramajeInterior = nz(p.getGramajeInterior(), 80);
|
||||
|
||||
// ===== Cubierta
|
||||
vm.cubierta.tipoCubierta = enumName(p.getTipoCubierta(), "tapaBlanda");
|
||||
vm.cubierta.solapasCubierta = Boolean.TRUE.equals(p.getSolapasCubierta()) ? 1 : 0;
|
||||
vm.cubierta.tamanioSolapasCubierta = String.valueOf(nz(p.getTamanioSolapasCubierta(), 80));
|
||||
vm.cubierta.cubiertaCaras = nz(p.getCubiertaCaras(), 2);
|
||||
|
||||
vm.cubierta.papelGuardasId = nz(p.getPapelGuardasId(), 3);
|
||||
vm.cubierta.gramajeGuardas = nz(p.getGramajeGuardas(), 170);
|
||||
vm.cubierta.guardasImpresas = nz(p.getGuardasImpresas(), 0);
|
||||
vm.cubierta.cabezada = nz(p.getCabezada(), "WHI");
|
||||
|
||||
vm.cubierta.papelCubiertaId = nz(p.getPapelCubiertaId(), 3);
|
||||
vm.cubierta.gramajeCubierta = nz(p.getGramajeCubierta(), 170);
|
||||
vm.cubierta.acabado = nz(p.getAcabado(), 1);
|
||||
|
||||
vm.cubierta.sobrecubierta.activo = Boolean.TRUE.equals(p.getSobrecubierta());
|
||||
vm.cubierta.sobrecubierta.papelSobrecubiertaId = nz(p.getPapelSobrecubiertaId(), 2);
|
||||
vm.cubierta.sobrecubierta.gramajeSobrecubierta = nz(p.getGramajeSobrecubierta(), 170);
|
||||
vm.cubierta.sobrecubierta.tamanioSolapasSobrecubierta = nz(p.getTamanioSolapasSobrecubierta(), 80);
|
||||
vm.cubierta.sobrecubierta.acabado = nz(p.getAcabadoSobrecubierta(), 0);
|
||||
|
||||
vm.cubierta.faja.activo = Boolean.TRUE.equals(p.getFaja());
|
||||
vm.cubierta.faja.papelFajaId = nz(p.getPapelFajaId(), 2);
|
||||
vm.cubierta.faja.gramajeFaja = nz(p.getGramajeFaja(), 170);
|
||||
vm.cubierta.faja.tamanioSolapasFaja = nz(p.getTamanioSolapasFaja(), 80);
|
||||
vm.cubierta.faja.acabado = nz(p.getAcabadoFaja(), 0);
|
||||
vm.cubierta.faja.alto = nz(p.getAltoFaja(), 50);
|
||||
|
||||
// ===== Selected tirada
|
||||
vm.selectedTirada = p.getSelectedTirada();
|
||||
|
||||
// ===== Servicios desde JSONs
|
||||
vm.servicios.servicios = parse(p.getServiciosJson(),
|
||||
new TypeReference<List<PresupuestoFormDataDto.Servicios.DatosServicios>>() {
|
||||
});
|
||||
if (vm.servicios.servicios == null)
|
||||
vm.servicios.servicios = new ArrayList<>();
|
||||
|
||||
// datos_maquetacion_json
|
||||
PresupuestoFormDataDto.Servicios.DatosMaquetacion maq = parse(p.getDatosMaquetacionJson(),
|
||||
PresupuestoFormDataDto.Servicios.DatosMaquetacion.class);
|
||||
if (maq != null)
|
||||
vm.servicios.datosMaquetacion = merge(vm.servicios.datosMaquetacion, maq);
|
||||
|
||||
// datos_marcapaginas_json
|
||||
PresupuestoFormDataDto.Servicios.DatosMarcapaginas mp = parse(p.getDatosMarcapaginasJson(),
|
||||
PresupuestoFormDataDto.Servicios.DatosMarcapaginas.class);
|
||||
if (mp != null)
|
||||
vm.servicios.datosMarcapaginas = merge(vm.servicios.datosMarcapaginas, mp);
|
||||
|
||||
return vm;
|
||||
}
|
||||
|
||||
// ===== Helpers =====
|
||||
|
||||
private static String enumName(Enum<?> e, String def) {
|
||||
return e != null ? e.name() : def;
|
||||
}
|
||||
|
||||
private static String nz(String v, String def) {
|
||||
return v != null ? v : def;
|
||||
}
|
||||
|
||||
private static Integer nz(Integer v, Integer def) {
|
||||
return v != null ? v : def;
|
||||
}
|
||||
|
||||
private static String nzStr(Integer v) {
|
||||
return v == null ? "" : String.valueOf(v);
|
||||
}
|
||||
|
||||
private <T> T parse(String json, Class<T> type) {
|
||||
try {
|
||||
if (json == null || json.isBlank())
|
||||
return null;
|
||||
return om.readValue(json, type);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T parse(String json, TypeReference<T> typeRef) {
|
||||
try {
|
||||
if (json == null || json.isBlank())
|
||||
return null;
|
||||
return om.readValue(json, typeRef);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T merge(T base, T override) {
|
||||
// merge muy simple: si override != null, devuélvelo; si no, base
|
||||
// (si quisieras merge campo a campo, usa BeanUtils o MapStruct)
|
||||
return override != null ? override : base;
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package com.imprimelibros.erp.presupuesto;
|
||||
package com.imprimelibros.erp.presupuesto.service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@ -8,11 +8,12 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.Locale;
|
||||
import java.text.NumberFormat;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
@ -21,8 +22,10 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
|
||||
import com.imprimelibros.erp.common.web.IpUtils;
|
||||
import com.imprimelibros.erp.configurationERP.VariableService;
|
||||
import com.imprimelibros.erp.presupuesto.Presupuesto.TipoCubierta;
|
||||
import com.imprimelibros.erp.presupuesto.GeoIpService;
|
||||
import com.imprimelibros.erp.presupuesto.PresupuestoRepository;
|
||||
import com.imprimelibros.erp.presupuesto.classes.ImagenPresupuesto;
|
||||
import com.imprimelibros.erp.presupuesto.classes.PresupuestadorItems;
|
||||
import com.imprimelibros.erp.presupuesto.classes.PresupuestoFormatter;
|
||||
@ -31,8 +34,15 @@ import com.imprimelibros.erp.presupuesto.maquetacion.MaquetacionPreciosRepositor
|
||||
import com.imprimelibros.erp.presupuesto.marcapaginas.Marcapaginas;
|
||||
import com.imprimelibros.erp.presupuesto.classes.PresupuestoMaquetacion;
|
||||
import com.imprimelibros.erp.presupuesto.classes.PresupuestoMarcapaginas;
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoCubierta;
|
||||
import com.imprimelibros.erp.presupuesto.maquetacion.MaquetacionMatrices;
|
||||
import com.imprimelibros.erp.presupuesto.maquetacion.MaquetacionMatricesRepository;
|
||||
import com.imprimelibros.erp.presupuesto.marcapaginas.MarcapaginasRepository;
|
||||
import com.imprimelibros.erp.users.UserDao;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import com.imprimelibros.erp.externalApi.skApiClient;
|
||||
|
||||
@Service
|
||||
@ -53,18 +63,25 @@ public class PresupuestoService {
|
||||
@Autowired
|
||||
protected MarcapaginasRepository marcapaginasRepository;
|
||||
|
||||
@Autowired
|
||||
protected PresupuestoRepository presupuestoRepository;
|
||||
|
||||
private final PresupuestadorItems presupuestadorItems;
|
||||
private final PresupuestoFormatter presupuestoFormatter;
|
||||
private final skApiClient apiClient;
|
||||
private final GeoIpService geoIpService;
|
||||
private final UserDao userRepo;
|
||||
|
||||
public PresupuestoService(PresupuestadorItems presupuestadorItems, PresupuestoFormatter presupuestoFormatter, skApiClient apiClient) {
|
||||
public PresupuestoService(PresupuestadorItems presupuestadorItems, PresupuestoFormatter presupuestoFormatter,
|
||||
skApiClient apiClient, GeoIpService geoIpService, UserDao userRepo) {
|
||||
this.presupuestadorItems = presupuestadorItems;
|
||||
this.presupuestoFormatter = presupuestoFormatter;
|
||||
this.apiClient = apiClient;
|
||||
this.geoIpService = geoIpService;
|
||||
this.userRepo = userRepo;
|
||||
}
|
||||
|
||||
public boolean validateDatosGenerales(int[] tiradas) {
|
||||
|
||||
for (int tirada : tiradas) {
|
||||
if (tirada <= 0) {
|
||||
return false; // Invalid tirada found
|
||||
@ -74,7 +91,6 @@ public class PresupuestoService {
|
||||
}
|
||||
|
||||
public Boolean isPOD(Presupuesto presupuesto) {
|
||||
|
||||
int pod_value = variableService.getValorEntero("POD");
|
||||
return (presupuesto.getTirada1() != null && presupuesto.getTirada1() <= pod_value) ||
|
||||
(presupuesto.getTirada2() != null && presupuesto.getTirada2() <= pod_value) ||
|
||||
@ -83,7 +99,6 @@ public class PresupuestoService {
|
||||
}
|
||||
|
||||
public Map<String, Object> obtenerOpcionesColor(Presupuesto presupuesto, Locale locale) {
|
||||
|
||||
List<ImagenPresupuesto> opciones = new ArrayList<>();
|
||||
|
||||
if (presupuesto.getPaginasColor() > 0) {
|
||||
@ -91,7 +106,10 @@ public class PresupuestoService {
|
||||
// POD solo color foto
|
||||
ImagenPresupuesto opcionColor = this.presupuestadorItems.getImpresionColor(locale);
|
||||
opcionColor.setSelected(Presupuesto.TipoImpresion.color.equals(presupuesto.getTipoImpresion()));
|
||||
if (Presupuesto.TipoImpresion.color.equals(presupuesto.getTipoImpresion()))
|
||||
opcionColor.setSelected(true);
|
||||
opciones.add(opcionColor);
|
||||
|
||||
}
|
||||
ImagenPresupuesto opcionColorHq = this.presupuestadorItems.getImpresionColorPremium(locale);
|
||||
if (Presupuesto.TipoImpresion.colorhq.equals(presupuesto.getTipoImpresion()))
|
||||
@ -102,6 +120,7 @@ public class PresupuestoService {
|
||||
if (Presupuesto.TipoImpresion.negro.equals(presupuesto.getTipoImpresion()))
|
||||
opcionNegro.setSelected(true);
|
||||
opciones.add(opcionNegro);
|
||||
|
||||
ImagenPresupuesto opcionNegroHq = this.presupuestadorItems.getImpresionNegroPremium(locale);
|
||||
if (Presupuesto.TipoImpresion.negrohq.equals(presupuesto.getTipoImpresion()))
|
||||
opcionNegroHq.setSelected(true);
|
||||
@ -121,16 +140,17 @@ public class PresupuestoService {
|
||||
}
|
||||
|
||||
public Map<String, Object> obtenerOpcionesPapelInterior(Presupuesto presupuesto, Locale locale) {
|
||||
|
||||
List<ImagenPresupuesto> opciones = new ArrayList<>();
|
||||
|
||||
opciones.add(this.presupuestadorItems.getPapelOffsetBlanco(locale));
|
||||
if (presupuesto.getTipoImpresion() == Presupuesto.TipoImpresion.negro ||
|
||||
if ((presupuesto.getTipoImpresion() == Presupuesto.TipoImpresion.negro
|
||||
&& !this.isPOD(presupuesto)) ||
|
||||
presupuesto.getTipoImpresion() == Presupuesto.TipoImpresion.color) {
|
||||
opciones.add(this.presupuestadorItems.getPapelOffsetBlancoVolumen(locale));
|
||||
}
|
||||
opciones.add(this.presupuestadorItems.getPapelOffsetAhuesado(locale));
|
||||
if (presupuesto.getTipoImpresion() == Presupuesto.TipoImpresion.negro ||
|
||||
if ((presupuesto.getTipoImpresion() == Presupuesto.TipoImpresion.negro
|
||||
&& !this.isPOD(presupuesto)) ||
|
||||
presupuesto.getTipoImpresion() == Presupuesto.TipoImpresion.color) {
|
||||
opciones.add(this.presupuestadorItems.getPapelOffsetAhuesadoVolumen(locale));
|
||||
}
|
||||
@ -144,7 +164,6 @@ public class PresupuestoService {
|
||||
}
|
||||
|
||||
boolean yaSeleccionado = opciones.stream().anyMatch(ImagenPresupuesto::isSelected);
|
||||
|
||||
if (!yaSeleccionado && !opciones.isEmpty()) {
|
||||
ImagenPresupuesto primeraOpcion = opciones.get(0);
|
||||
primeraOpcion.setSelected(true);
|
||||
@ -153,12 +172,10 @@ public class PresupuestoService {
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("opciones_papel_interior", opciones);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public Map<String, Object> obtenerOpcionesGramajeInterior(Presupuesto presupuesto) {
|
||||
|
||||
List<String> gramajes = new ArrayList<>();
|
||||
|
||||
final int BLANCO_OFFSET_ID = 3;
|
||||
@ -168,7 +185,6 @@ public class PresupuestoService {
|
||||
final int ESTUCADO_MATE_ID = 2;
|
||||
|
||||
if (presupuesto.getPapelInteriorId() != null && presupuesto.getPapelInteriorId() == BLANCO_OFFSET_ID) {
|
||||
|
||||
gramajes.add("80");
|
||||
gramajes.add("90");
|
||||
if (presupuesto.getTipoImpresion() == Presupuesto.TipoImpresion.negrohq ||
|
||||
@ -180,32 +196,25 @@ public class PresupuestoService {
|
||||
}
|
||||
} else if (presupuesto.getPapelInteriorId() != null
|
||||
&& presupuesto.getPapelInteriorId() == BLANCO_OFFSET_VOLUMEN_ID) {
|
||||
|
||||
if (presupuesto.getTipoImpresion() == Presupuesto.TipoImpresion.negro ||
|
||||
presupuesto.getTipoImpresion() == Presupuesto.TipoImpresion.color) {
|
||||
gramajes.add("80");
|
||||
}
|
||||
|
||||
} else if (presupuesto.getPapelInteriorId() != null && presupuesto.getPapelInteriorId() == AHUESADO_OFFSET_ID) {
|
||||
|
||||
gramajes.add("80");
|
||||
gramajes.add("90");
|
||||
if (presupuesto.getTipoImpresion() == Presupuesto.TipoImpresion.negrohq ||
|
||||
presupuesto.getTipoImpresion() == Presupuesto.TipoImpresion.colorhq) {
|
||||
gramajes.add("100");
|
||||
}
|
||||
|
||||
} else if (presupuesto.getPapelInteriorId() != null
|
||||
&& presupuesto.getPapelInteriorId() == AHUESADO_OFFSET_VOLUMEN_ID) {
|
||||
|
||||
if (presupuesto.getTipoImpresion() == Presupuesto.TipoImpresion.negro ||
|
||||
presupuesto.getTipoImpresion() == Presupuesto.TipoImpresion.color) {
|
||||
gramajes.add("70");
|
||||
gramajes.add("80");
|
||||
}
|
||||
|
||||
} else if (presupuesto.getPapelInteriorId() != null && presupuesto.getPapelInteriorId() == ESTUCADO_MATE_ID) {
|
||||
|
||||
if (presupuesto.getTipoImpresion() != Presupuesto.TipoImpresion.color) {
|
||||
gramajes.add("90");
|
||||
}
|
||||
@ -214,7 +223,8 @@ public class PresupuestoService {
|
||||
gramajes.add("100");
|
||||
gramajes.add("115");
|
||||
}
|
||||
if (presupuesto.getTipoImpresion() == Presupuesto.TipoImpresion.negro ||
|
||||
if ((presupuesto.getTipoImpresion() == Presupuesto.TipoImpresion.negro
|
||||
&& !this.isPOD(presupuesto)) ||
|
||||
presupuesto.getTipoImpresion() == Presupuesto.TipoImpresion.color) {
|
||||
gramajes.add("120");
|
||||
}
|
||||
@ -234,7 +244,6 @@ public class PresupuestoService {
|
||||
}
|
||||
|
||||
public Map<String, Object> obtenerOpcionesPapelCubierta(Presupuesto presupuesto, Locale locale) {
|
||||
|
||||
List<ImagenPresupuesto> opciones = new ArrayList<>();
|
||||
|
||||
if (presupuesto.getTipoCubierta() == Presupuesto.TipoCubierta.tapaBlanda) {
|
||||
@ -255,7 +264,6 @@ public class PresupuestoService {
|
||||
}
|
||||
|
||||
public Map<String, Object> obtenerOpcionesGramajeCubierta(Presupuesto presupuesto) {
|
||||
|
||||
List<String> gramajes = new ArrayList<>();
|
||||
|
||||
final int CARTULINA_GRAFICA_ID = 5;
|
||||
@ -268,7 +276,6 @@ public class PresupuestoService {
|
||||
gramajes.add("350");
|
||||
} else if (presupuesto.getPapelCubiertaId() != null && presupuesto.getPapelCubiertaId() == ESTUCADO_MATE_ID) {
|
||||
if (presupuesto.getTipoCubierta() == Presupuesto.TipoCubierta.tapaBlanda) {
|
||||
|
||||
gramajes.add("250");
|
||||
gramajes.add("300");
|
||||
gramajes.add("350");
|
||||
@ -283,7 +290,6 @@ public class PresupuestoService {
|
||||
}
|
||||
|
||||
public Map<String, Object> toSkApiRequest(Presupuesto presupuesto) {
|
||||
|
||||
final int SK_CLIENTE_ID = 1284;
|
||||
final int SK_PAGINAS_CUADERNILLO = 32;
|
||||
|
||||
@ -343,7 +349,6 @@ public class PresupuestoService {
|
||||
}
|
||||
|
||||
public Integer getTipoImpresionId(Presupuesto presupuesto) {
|
||||
|
||||
if (presupuesto.getTipoEncuadernacion() == Presupuesto.TipoEncuadernacion.fresado) {
|
||||
if (presupuesto.getTipoCubierta() == Presupuesto.TipoCubierta.tapaDura ||
|
||||
presupuesto.getTipoCubierta() == Presupuesto.TipoCubierta.tapaDuraLomoRedondo) {
|
||||
@ -437,12 +442,6 @@ public class PresupuestoService {
|
||||
return resultado;
|
||||
}
|
||||
|
||||
public Map<String, Object> aplicarMargenTiradas(Map<String, Object> data) {
|
||||
|
||||
// implementar margenes
|
||||
return (Map<String, Object>) data;
|
||||
}
|
||||
|
||||
public String obtenerPrecioRetractilado(Presupuesto presupuesto, Locale locale) {
|
||||
Integer[] tiradas = presupuesto.getTiradas();
|
||||
Integer tirada_min = Arrays.stream(tiradas)
|
||||
@ -454,13 +453,21 @@ public class PresupuestoService {
|
||||
presupuesto.getSelectedTirada() != null ? presupuesto.getSelectedTirada() : tirada_min);
|
||||
Double precio_retractilado = apiClient.getRetractilado(requestBody);
|
||||
return precio_retractilado != null
|
||||
? NumberFormat.getNumberInstance(locale)
|
||||
.format(Math.round(precio_retractilado * 100.0) / 100.0)
|
||||
: "0,00";
|
||||
? String.valueOf(Math.round(precio_retractilado * 100.0) / 100.0)
|
||||
: "0.00";
|
||||
}
|
||||
|
||||
private String obtenerPrecioRetractilado(Integer tirada) {
|
||||
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("tirada", tirada != null ? tirada : 0);
|
||||
Double precio_retractilado = apiClient.getRetractilado(requestBody);
|
||||
return precio_retractilado != null
|
||||
? String.valueOf(Math.round(precio_retractilado * 100.0) / 100.0)
|
||||
: "0.00";
|
||||
}
|
||||
|
||||
public Map<String, Object> obtenerServiciosExtras(Presupuesto presupuesto, Locale locale) {
|
||||
|
||||
List<Object> opciones = new ArrayList<>();
|
||||
|
||||
Double price_prototipo = this.obtenerPrototipo(presupuesto);
|
||||
@ -532,8 +539,8 @@ public class PresupuestoService {
|
||||
put("price", messageSource.getMessage("presupuesto.consultar-soporte", null, locale));
|
||||
put("priceUnit", "");
|
||||
} else {
|
||||
put("price", NumberFormat.getNumberInstance(locale)
|
||||
.format(Math.round(price_prototipo * 100.0) / 100.0));
|
||||
put("price",
|
||||
String.valueOf(Math.round(price_prototipo * 100.0) / 100.0));
|
||||
put("priceUnit", messageSource.getMessage("app.currency-symbol", null, locale));
|
||||
}
|
||||
}
|
||||
@ -560,42 +567,10 @@ public class PresupuestoService {
|
||||
return response;
|
||||
}
|
||||
|
||||
private Double obtenerPrototipo(Presupuesto presupuesto) {
|
||||
|
||||
// Obtenemos el precio de 1 unidad para el ejemplar de prueba
|
||||
HashMap<String, Object> price = new HashMap<>();
|
||||
// make a copy of "presupuesto" to avoid modifying the original object
|
||||
Presupuesto presupuestoTemp = presupuesto.clone();
|
||||
presupuestoTemp.setTirada1(1);
|
||||
presupuestoTemp.setTirada2(null);
|
||||
presupuestoTemp.setTirada3(null);
|
||||
presupuestoTemp.setTirada4(null);
|
||||
if (presupuestoTemp.getTipoImpresion() == Presupuesto.TipoImpresion.color) {
|
||||
presupuestoTemp.setTipoImpresion(Presupuesto.TipoImpresion.colorhq);
|
||||
} else if (presupuestoTemp.getTipoImpresion() == Presupuesto.TipoImpresion.negro) {
|
||||
presupuestoTemp.setTipoImpresion(Presupuesto.TipoImpresion.negrohq);
|
||||
}
|
||||
String priceStr = apiClient.getPrice(this.toSkApiRequest(presupuestoTemp), presupuestoTemp.getTipoEncuadernacion(), presupuestoTemp.getTipoCubierta());
|
||||
Double price_prototipo = 0.0;
|
||||
try {
|
||||
price = new ObjectMapper().readValue(priceStr, new TypeReference<>() {
|
||||
});
|
||||
price_prototipo = ((List<Double>) ((Map<String, Object>) price.get("data")).get("precios")).get(0);
|
||||
if (price_prototipo < 25) {
|
||||
price_prototipo = 25.0;
|
||||
}
|
||||
|
||||
} catch (JsonProcessingException e) {
|
||||
} catch (Exception exception) {
|
||||
}
|
||||
return price_prototipo;
|
||||
}
|
||||
|
||||
public HashMap<String, Object> getPrecioMaquetacion(PresupuestoMaquetacion presupuestoMaquetacion, Locale locale) {
|
||||
try {
|
||||
List<MaquetacionPrecios> lista = maquetacionPreciosRepository.findAll();
|
||||
|
||||
// helper para obtener un precio por clave
|
||||
java.util.function.Function<String, Double> price = key -> lista.stream()
|
||||
.filter(p -> key.equals(p.getKey()))
|
||||
.map(MaquetacionPrecios::getValue)
|
||||
@ -604,13 +579,10 @@ public class PresupuestoService {
|
||||
|
||||
BigDecimal precio = BigDecimal.ZERO;
|
||||
|
||||
// millar_maquetacion * (numCaracteres / 1000.0)
|
||||
BigDecimal millares = BigDecimal.valueOf(presupuestoMaquetacion.getNumCaracteres()).divide(
|
||||
BigDecimal.valueOf(1000), 6,
|
||||
RoundingMode.HALF_UP);
|
||||
BigDecimal.valueOf(1000), 6, RoundingMode.HALF_UP);
|
||||
precio = precio.add(millares.multiply(BigDecimal.valueOf(price.apply("millar_maquetacion"))));
|
||||
|
||||
// Numero de paginas estimado
|
||||
int numPaginas = 0;
|
||||
Integer matricesPorPagina = maquetacionMatricesRepository.findMatrices(
|
||||
presupuestoMaquetacion.getFormato(),
|
||||
@ -618,7 +590,7 @@ public class PresupuestoService {
|
||||
if (matricesPorPagina != null && matricesPorPagina > 0) {
|
||||
numPaginas = presupuestoMaquetacion.getNumCaracteres() / matricesPorPagina;
|
||||
}
|
||||
// Precio por pagina estimado
|
||||
|
||||
BigDecimal precioRedondeado = precio.setScale(2, RoundingMode.HALF_UP);
|
||||
double precioPaginaEstimado = 0.0;
|
||||
if (numPaginas > 0) {
|
||||
@ -627,7 +599,6 @@ public class PresupuestoService {
|
||||
.doubleValue();
|
||||
}
|
||||
|
||||
// tabla, columna, foto
|
||||
precio = precio
|
||||
.add(BigDecimal.valueOf(presupuestoMaquetacion.getNumTablas())
|
||||
.multiply(BigDecimal.valueOf(price.apply("tabla"))));
|
||||
@ -652,7 +623,6 @@ public class PresupuestoService {
|
||||
precio = precio.add(BigDecimal.valueOf(price.apply("epub")));
|
||||
}
|
||||
|
||||
// redondeo final
|
||||
precioRedondeado = precio.setScale(2, RoundingMode.HALF_UP);
|
||||
|
||||
HashMap<String, Object> out = new HashMap<>();
|
||||
@ -684,9 +654,7 @@ public class PresupuestoService {
|
||||
|
||||
public HashMap<String, Object> getPrecioMarcapaginas(PresupuestoMarcapaginas presupuestoMarcapaginas,
|
||||
Locale locale) {
|
||||
|
||||
try {
|
||||
|
||||
List<Marcapaginas> m = marcapaginasRepository.findPrecios(presupuestoMarcapaginas);
|
||||
if (m.isEmpty() || m.get(0) == null) {
|
||||
HashMap<String, Object> out = new HashMap<>();
|
||||
@ -774,47 +742,60 @@ public class PresupuestoService {
|
||||
return out;
|
||||
}
|
||||
|
||||
public Map<String, Object> getResumen(Presupuesto presupuesto, List<Map<String, Object>> servicios, Locale locale) {
|
||||
|
||||
/**
|
||||
* Calcula el resumen (SIN persistir cambios de estado).
|
||||
* Mantiene firma para no romper llamadas existentes.
|
||||
*/
|
||||
public Map<String, Object> getTextosResumen(Presupuesto presupuesto, List<Map<String, Object>> servicios,
|
||||
Map<String, Object> datosMaquetacion, Map<String, Object> datosMarcapaginas, Locale locale) {
|
||||
Map<String, Object> resumen = new HashMap<>();
|
||||
resumen.put("titulo", presupuesto.getTitulo());
|
||||
|
||||
Presupuesto pressupuestoTemp = presupuesto.clone();
|
||||
|
||||
resumen.put("imagen", "/assets/images/imprimelibros/presupuestador/" + presupuesto.getTipoEncuadernacion() + ".png");
|
||||
resumen.put("imagen_alt", messageSource.getMessage("presupuesto." + presupuesto.getTipoEncuadernacion(), null, locale));
|
||||
resumen.put("imagen",
|
||||
"/assets/images/imprimelibros/presupuestador/" + presupuesto.getTipoEncuadernacion() + ".png");
|
||||
resumen.put("imagen_alt",
|
||||
messageSource.getMessage("presupuesto." + presupuesto.getTipoEncuadernacion(), null, locale));
|
||||
|
||||
boolean hayDepositoLegal = servicios != null && servicios.stream()
|
||||
.map(m -> java.util.Objects.toString(m.get("id"), "")) // null-safe -> String
|
||||
.map(m -> java.util.Objects.toString(m.get("id"), ""))
|
||||
.map(String::trim)
|
||||
.anyMatch("deposito-legal"::equals);
|
||||
|
||||
if(hayDepositoLegal){
|
||||
pressupuestoTemp.setSelectedTirada(presupuesto.getSelectedTirada()+4);
|
||||
if (hayDepositoLegal) {
|
||||
pressupuestoTemp.setSelectedTirada(
|
||||
presupuesto.getSelectedTirada() != null ? presupuesto.getSelectedTirada() + 4 : 4);
|
||||
for (Integer i = 0; i < pressupuestoTemp.getTiradas().length; i++) {
|
||||
Integer tirada = pressupuestoTemp.getTiradas()[i];
|
||||
if (tirada != null && tirada >= 4) {
|
||||
tirada = tirada + 4;
|
||||
}
|
||||
pressupuestoTemp.getTiradas()[i] = tirada;
|
||||
}
|
||||
}
|
||||
|
||||
HashMap<String, Object> precios = this.calcularPresupuesto(pressupuestoTemp, locale);
|
||||
if(precios.containsKey("error")){
|
||||
if (precios.containsKey("error")) {
|
||||
resumen.put("error", precios.get("error"));
|
||||
return resumen;
|
||||
}
|
||||
|
||||
resumen.put("precios", precios);
|
||||
|
||||
HashMap<String, Object> linea = new HashMap<>();
|
||||
Double precio_unitario = 0.0;
|
||||
Double precio_total = 0.0;
|
||||
Integer counter = 0;
|
||||
linea.put("descripcion", presupuestoFormatter.resumen(presupuesto, servicios, locale));
|
||||
linea.put("cantidad", presupuesto.getSelectedTirada() != null ? presupuesto.getSelectedTirada() : 0);
|
||||
precio_unitario = ((List<Double>) ((Map<String, Object>) precios.get("data")).get("precios"))
|
||||
.get(0);
|
||||
precio_total = precio_unitario * presupuesto.getSelectedTirada();
|
||||
precio_unitario = ((List<Double>) ((Map<String, Object>) precios.get("data")).get("precios")).get(0);
|
||||
precio_total = precio_unitario
|
||||
* (presupuesto.getSelectedTirada() != null ? presupuesto.getSelectedTirada() : 0);
|
||||
linea.put("precio_unitario", precio_unitario);
|
||||
linea.put("precio_total", BigDecimal.valueOf(precio_total).setScale(2, RoundingMode.HALF_UP));
|
||||
resumen.put("linea" + counter, linea);
|
||||
counter++;
|
||||
|
||||
if(hayDepositoLegal) {
|
||||
|
||||
if (hayDepositoLegal) {
|
||||
linea = new HashMap<>();
|
||||
linea.put("descripcion", messageSource.getMessage("presupuesto.resumen-deposito-legal", null, locale));
|
||||
linea.put("cantidad", 4);
|
||||
@ -825,15 +806,69 @@ public class PresupuestoService {
|
||||
}
|
||||
|
||||
List<Map<String, Object>> serviciosExtras = new ArrayList<>();
|
||||
|
||||
if(servicios != null){
|
||||
if (servicios != null) {
|
||||
for (Map<String, Object> servicio : servicios) {
|
||||
HashMap<String, Object> servicioData = new HashMap<>();
|
||||
servicioData.put("id", servicio.get("id"));
|
||||
servicioData.put("descripcion", servicio.get("label"));
|
||||
servicioData.put("precio", servicio.get("id").equals("marcapaginas") ?
|
||||
Double.parseDouble(servicio.get("price").toString())/Double.parseDouble(servicio.get("units").toString()) :
|
||||
servicio.get("price"));
|
||||
if (servicio.get("id").equals("marcapaginas")) {
|
||||
String descripcion = servicio.get("label").toString();
|
||||
descripcion += "<br><ul><li>";
|
||||
descripcion += Marcapaginas.Tamanios.valueOf(datosMarcapaginas.get("tamanio").toString()).getLabel()
|
||||
+ ", ";
|
||||
descripcion += Marcapaginas.Caras_Impresion
|
||||
.valueOf(datosMarcapaginas.get("carasImpresion").toString()).getMessageKey() + ", ";
|
||||
descripcion += messageSource
|
||||
.getMessage(Marcapaginas.Papeles.valueOf(datosMarcapaginas.get("papel").toString())
|
||||
.getMessageKey(), null, locale)
|
||||
+ " - " +
|
||||
datosMarcapaginas.get("gramaje").toString() + " gr, ";
|
||||
descripcion += messageSource.getMessage(
|
||||
Marcapaginas.Acabado.valueOf(datosMarcapaginas.get("acabado").toString()).getMessageKey(),
|
||||
null, locale);
|
||||
descripcion += "</li></ul>";
|
||||
servicioData.put("descripcion", descripcion);
|
||||
|
||||
} else if (servicio.get("id").equals("maquetacion")) {
|
||||
String descripcion = servicio.get("label").toString();
|
||||
descripcion += "<br><ul><li>";
|
||||
descripcion += (datosMaquetacion.get("num_caracteres") + " "
|
||||
+ messageSource.getMessage("presupuesto.maquetacion.caracteres", null, locale)) + ", ";
|
||||
descripcion += MaquetacionMatrices.Formato
|
||||
.valueOf(datosMaquetacion.get("formato_maquetacion").toString()).getLabel() + ", ";
|
||||
descripcion += messageSource.getMessage(MaquetacionMatrices.FontSize
|
||||
.valueOf(datosMaquetacion.get("cuerpo_texto").toString()).getMessageKey(), null, locale)
|
||||
+ ", ";
|
||||
descripcion += messageSource.getMessage("presupuesto.maquetacion.num-columnas", null, locale) + ": "
|
||||
+ datosMaquetacion.get("num_columnas").toString() + ", ";
|
||||
descripcion += messageSource.getMessage("presupuesto.maquetacion.num-tablas", null, locale) + ": "
|
||||
+ datosMaquetacion.get("num_tablas").toString() + ", ";
|
||||
descripcion += messageSource.getMessage("presupuesto.maquetacion.num-fotos", null, locale) + ": "
|
||||
+ datosMaquetacion.get("num_fotos").toString();
|
||||
if ((boolean) datosMaquetacion.get("correccion_ortotipografica")) {
|
||||
descripcion += ", " + messageSource
|
||||
.getMessage("presupuesto.maquetacion.correccion-ortotipografica", null, locale);
|
||||
}
|
||||
if ((boolean) datosMaquetacion.get("texto_mecanografiado")) {
|
||||
descripcion += ", " + messageSource.getMessage("presupuesto.maquetacion.texto-mecanografiado",
|
||||
null, locale);
|
||||
}
|
||||
if ((boolean) datosMaquetacion.get("disenio_portada")) {
|
||||
descripcion += ", "
|
||||
+ messageSource.getMessage("presupuesto.maquetacion.diseno-portada", null, locale);
|
||||
}
|
||||
if ((boolean) datosMaquetacion.get("epub")) {
|
||||
descripcion += ", " + messageSource.getMessage("presupuesto.maquetacion.epub", null, locale);
|
||||
}
|
||||
descripcion += "</li></ul>";
|
||||
servicioData.put("descripcion", descripcion);
|
||||
} else {
|
||||
servicioData.put("descripcion", servicio.get("label"));
|
||||
}
|
||||
|
||||
servicioData.put("precio", servicio.get("id").equals("marcapaginas")
|
||||
? Double.parseDouble(servicio.get("price").toString())
|
||||
/ Double.parseDouble(servicio.get("units").toString())
|
||||
: servicio.get("price"));
|
||||
servicioData.put("unidades", servicio.get("units"));
|
||||
serviciosExtras.add(servicioData);
|
||||
}
|
||||
@ -843,10 +878,343 @@ public class PresupuestoService {
|
||||
return resumen;
|
||||
}
|
||||
|
||||
public HashMap<String, Object> calcularPresupuesto(Presupuesto presupuesto, Locale locale) {
|
||||
/**
|
||||
* PÚBLICO: calcula el resumen y GUARDA el presupuesto completo como BORRADOR.
|
||||
* Se invoca al entrar en la pestaña "Resumen" del presupuestador público.
|
||||
*/
|
||||
// PresupuestoService.java
|
||||
public Map<String, Object> getResumen(
|
||||
Presupuesto presupuesto,
|
||||
List<Map<String, Object>> servicios,
|
||||
Map<String, Object> datosMaquetacion,
|
||||
Map<String, Object> datosMarcapaginas,
|
||||
Boolean save,
|
||||
String mode,
|
||||
Locale locale,
|
||||
String sessionId,
|
||||
String ip) {
|
||||
|
||||
// 1) Calcula el resumen (como ya haces)
|
||||
try {
|
||||
presupuesto.setDatosMaquetacionJson(
|
||||
datosMaquetacion != null ? new ObjectMapper().writeValueAsString(datosMaquetacion) : null);
|
||||
presupuesto.setDatosMarcapaginasJson(
|
||||
datosMarcapaginas != null ? new ObjectMapper().writeValueAsString(datosMarcapaginas) : null);
|
||||
} catch (Exception e) {
|
||||
System.out.println("Error guardando datos adicionales: " + e.getMessage());
|
||||
}
|
||||
|
||||
Map<String, Object> resumen = getTextosResumen(presupuesto, servicios, datosMaquetacion, datosMarcapaginas,
|
||||
locale);
|
||||
if (resumen.containsKey("error"))
|
||||
return resumen;
|
||||
|
||||
presupuesto = generateTotalizadores(presupuesto, servicios, resumen, locale);
|
||||
|
||||
// 3) Enriquecer el Presupuesto a persistir
|
||||
presupuesto.setEstado(Presupuesto.Estado.borrador);
|
||||
if (mode.equals("public")) {
|
||||
|
||||
presupuesto = getDatosLocalizacion(presupuesto, sessionId, ip);
|
||||
presupuesto.setOrigen(Presupuesto.Origen.publico);
|
||||
presupuesto = this.getDatosLocalizacion(presupuesto, sessionId, ip);
|
||||
|
||||
} else
|
||||
presupuesto.setOrigen(Presupuesto.Origen.privado);
|
||||
|
||||
// 4) UPSERT: si viene id -> actualiza; si no, reusa el último borrador de la
|
||||
// sesión
|
||||
Presupuesto entidad;
|
||||
if (presupuesto.getId() != null) {
|
||||
entidad = presupuestoRepository.findById(presupuesto.getId()).orElse(presupuesto);
|
||||
} else {
|
||||
entidad = presupuestoRepository
|
||||
.findTopBySessionIdAndEstadoOrderByCreatedAtDesc(sessionId, Presupuesto.Estado.borrador)
|
||||
.orElse(presupuesto);
|
||||
// Si se reutiliza un borrador existente, copia el ID a nuestro objeto para
|
||||
// hacer merge
|
||||
presupuesto.setId(entidad.getId());
|
||||
}
|
||||
|
||||
// 5) Guardar/actualizar
|
||||
entidad = mergePresupuesto(entidad, presupuesto);
|
||||
|
||||
if (save != null && save) {
|
||||
|
||||
// Si NO es para guardar (solo calcular resumen), devolver sin persistir
|
||||
presupuestoRepository.saveAndFlush(presupuesto);
|
||||
}
|
||||
|
||||
// Opcional: devolver el id guardado al frontend para que lo envíe en llamadas
|
||||
// siguientes
|
||||
resumen.put("presupuesto_id", entidad.getId());
|
||||
resumen.put("precio_unitario", presupuesto.getPrecioUnitario());
|
||||
resumen.put("precio_total_tirada", presupuesto.getPrecioTotalTirada());
|
||||
resumen.put("servicios_total", presupuesto.getServiciosTotal());
|
||||
resumen.put("base_imponible", presupuesto.getBaseImponible());
|
||||
resumen.put("iva_importe_4", presupuesto.getIvaImporte4());
|
||||
resumen.put("iva_importe_21", presupuesto.getIvaImporte21());
|
||||
resumen.put("total_con_iva", presupuesto.getTotalConIva());
|
||||
|
||||
return resumen;
|
||||
}
|
||||
|
||||
public Presupuesto getDatosLocalizacion(Presupuesto presupuesto, String sessionId, String ip) {
|
||||
|
||||
presupuesto.setOrigen(Presupuesto.Origen.publico);
|
||||
presupuesto.setSessionId(sessionId);
|
||||
// IP: guarda hash y trunc (si tienes campos). Si no, guarda tal cual en
|
||||
// ip_trunc/ip_hash según tu modelo.
|
||||
String ipTrunc = anonymizeIp(ip);
|
||||
presupuesto.setIpTrunc(ipTrunc);
|
||||
presupuesto.setIpHash(Integer.toHexString(ip.hashCode()));
|
||||
|
||||
// ubicación (si tienes un servicio GeoIP disponible; si no, omite estas tres
|
||||
// líneas)
|
||||
try {
|
||||
GeoIpService.GeoData geo = geoIpService.lookup(ip).orElse(null);
|
||||
presupuesto.setPais(geo.getPais());
|
||||
presupuesto.setRegion(geo.getRegion());
|
||||
presupuesto.setCiudad(geo.getCiudad());
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
return presupuesto;
|
||||
}
|
||||
|
||||
public Presupuesto generateTotalizadores(
|
||||
Presupuesto presupuesto,
|
||||
List<Map<String, Object>> servicios,
|
||||
Map<String, Object> resumen,
|
||||
Locale locale) {
|
||||
|
||||
Map<Integer, Map<String, Object>> pricing_snapshot = new HashMap<>();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> preciosNode = (Map<String, Object>) resumen.getOrDefault("precios", Map.of());
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> data = (Map<String, Object>) preciosNode.getOrDefault("data", Map.of());
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Integer> tiradas = (List<Integer>) data.getOrDefault("tiradas", List.of());
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Double> precios = (List<Double>) data.getOrDefault("precios", List.of());
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Double> pesos = (List<Double>) data.getOrDefault("peso", List.of());
|
||||
|
||||
boolean hayDepositoLegal = servicios != null && servicios.stream()
|
||||
.map(m -> java.util.Objects.toString(m.get("id"), ""))
|
||||
.map(String::trim)
|
||||
.anyMatch("deposito-legal"::equals);
|
||||
|
||||
if (precios.isEmpty()) {
|
||||
var preciosCalc = this.calcularPresupuesto(presupuesto, locale);
|
||||
precios = (List<Double>) ((Map<String, Object>) preciosCalc.get("data")).getOrDefault("precios", List.of());
|
||||
}
|
||||
|
||||
// iterate getTiradas with a foreach with not null
|
||||
for (Integer tirada : presupuesto.getTiradas()) {
|
||||
if (tirada == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Genera los totalizadores (precio unitario, total tirada, etc.) sin guardar
|
||||
double precioUnit = 0.0;
|
||||
int cantidad = tirada != null ? tirada : 0;
|
||||
int index = tiradas.indexOf(tirada);
|
||||
try {
|
||||
|
||||
if (index >= 0 && index < precios.size()) {
|
||||
precioUnit = precios.get(index);
|
||||
} else if (!precios.isEmpty()) {
|
||||
precioUnit = precios.get(0); // fallback al primero
|
||||
}
|
||||
// guarda el snapshot completo de precios para auditoría
|
||||
presupuesto.setPreciosPorTiradaJson(new ObjectMapper().writeValueAsString(precios));
|
||||
} catch (Exception ignore) {
|
||||
precioUnit = 0.0;
|
||||
}
|
||||
|
||||
BigDecimal precioTotalTirada = BigDecimal.valueOf(precioUnit)
|
||||
.multiply(BigDecimal.valueOf(cantidad))
|
||||
.setScale(2, RoundingMode.HALF_UP);
|
||||
if (hayDepositoLegal) {
|
||||
precioTotalTirada = precioTotalTirada
|
||||
.add(BigDecimal.valueOf(precioUnit).multiply(BigDecimal.valueOf(4)))
|
||||
.setScale(6, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
// servicios_total
|
||||
BigDecimal serviciosIva4 = BigDecimal.ZERO;
|
||||
BigDecimal serviciosTotal = BigDecimal.ZERO;
|
||||
if (servicios != null) {
|
||||
for (Map<String, Object> s : servicios) {
|
||||
try {
|
||||
// retractilado: recalcular precio
|
||||
if (s.get("id").equals("retractilado")) {
|
||||
double precio_retractilado = obtenerPrecioRetractilado(cantidad) != null
|
||||
? Double.parseDouble(obtenerPrecioRetractilado(cantidad))
|
||||
: 0.0;
|
||||
s.put("price", precio_retractilado);
|
||||
}
|
||||
// si tiene protitipo, guardamos el valor para el IVA al 4%
|
||||
else if (s.get("id").equals("ejemplar-prueba")) {
|
||||
serviciosIva4 = BigDecimal.valueOf(
|
||||
s.get("price") != null ? Double.parseDouble(String.valueOf(s.get("price"))) : 0.0);
|
||||
}
|
||||
double unidades = Double.parseDouble(String.valueOf(s.getOrDefault("units", 0)));
|
||||
double precio = Double.parseDouble(String.valueOf(
|
||||
s.get("id").equals("marcapaginas")
|
||||
? (Double.parseDouble(String.valueOf(s.get("price"))) / unidades) // unidad
|
||||
: s.getOrDefault("price", 0)));
|
||||
serviciosTotal = serviciosTotal.add(
|
||||
BigDecimal.valueOf(precio).multiply(BigDecimal.valueOf(unidades)));
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
try {
|
||||
presupuesto.setServiciosJson(new ObjectMapper().writeValueAsString(servicios));
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
BigDecimal baseImponible = precioTotalTirada;
|
||||
BigDecimal ivaImporte4 = BigDecimal.ZERO;
|
||||
BigDecimal ivaImporte21 = BigDecimal.ZERO;
|
||||
|
||||
// Si la entrega es en peninsula, se mira el valor del iva
|
||||
// Canarias y paises UE no llevan IVA
|
||||
if (presupuesto.getEntregaTipo() == Presupuesto.Entrega.peninsula) {
|
||||
// Si el iva es reducido, el precio de la tirada y el del prototipo llevan IVA
|
||||
// 4%
|
||||
if (presupuesto.getIvaReducido()) {
|
||||
ivaImporte4 = baseImponible.add(serviciosIva4).multiply(BigDecimal.valueOf(4)).divide(
|
||||
BigDecimal.valueOf(100), 2,
|
||||
RoundingMode.HALF_UP);
|
||||
ivaImporte21 = serviciosTotal.subtract(serviciosIva4).multiply(BigDecimal.valueOf(21)).divide(
|
||||
BigDecimal.valueOf(100), 2,
|
||||
RoundingMode.HALF_UP);
|
||||
} else {
|
||||
ivaImporte21 = baseImponible.add(serviciosTotal).multiply(BigDecimal.valueOf(21)).divide(
|
||||
BigDecimal.valueOf(100), 2,
|
||||
RoundingMode.HALF_UP);
|
||||
}
|
||||
}
|
||||
baseImponible = baseImponible.add(serviciosTotal);
|
||||
BigDecimal totalConIva = baseImponible.add(ivaImporte21).add(ivaImporte4);
|
||||
|
||||
// precios y totales
|
||||
if (tirada == (presupuesto.getSelectedTirada() != null ? presupuesto.getSelectedTirada() : 0)) {
|
||||
presupuesto.setPrecioUnitario(BigDecimal.valueOf(precioUnit).setScale(6, RoundingMode.HALF_UP));
|
||||
presupuesto.setPrecioTotalTirada(precioTotalTirada);
|
||||
presupuesto.setServiciosTotal(serviciosTotal);
|
||||
presupuesto.setBaseImponible(baseImponible);
|
||||
presupuesto.setIvaImporte4(ivaImporte4);
|
||||
presupuesto.setIvaImporte21(ivaImporte21);
|
||||
presupuesto.setTotalConIva(totalConIva);
|
||||
}
|
||||
Map<String, Object> snap = new HashMap<>();
|
||||
snap.put("precio_unitario", BigDecimal.valueOf(precioUnit).setScale(6, RoundingMode.HALF_UP));
|
||||
snap.put("precio_total_tirada", precioTotalTirada);
|
||||
snap.put("servicios_total", serviciosTotal);
|
||||
snap.put("base_imponible", baseImponible);
|
||||
snap.put("iva_importe_4", ivaImporte4);
|
||||
snap.put("iva_importe_21", ivaImporte21);
|
||||
snap.put("total_con_iva", totalConIva);
|
||||
snap.put("peso", (index >= 0 && index < pesos.size()) ? pesos.get(index) : 0.0);
|
||||
|
||||
pricing_snapshot.put(tirada, snap);
|
||||
|
||||
}
|
||||
|
||||
try {
|
||||
String json = new ObjectMapper()
|
||||
.writer()
|
||||
.withDefaultPrettyPrinter() // opcional
|
||||
.writeValueAsString(pricing_snapshot);
|
||||
presupuesto.setPricingSnapshotJson(pricing_snapshot.isEmpty() ? null : json);
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
|
||||
return presupuesto;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public HashMap<String, Object> guardarPresupuesto(
|
||||
Presupuesto presupuesto,
|
||||
List<Map<String, Object>> serviciosList,
|
||||
Map<String, Object> datosMaquetacion,
|
||||
Map<String, Object> datosMarcapaginas,
|
||||
String mode,
|
||||
Long cliente_id,
|
||||
Long id,
|
||||
HttpServletRequest request,
|
||||
Locale locale) {
|
||||
|
||||
HashMap<String, Object> result = new HashMap<>();
|
||||
try {
|
||||
|
||||
presupuesto.setDatosMaquetacionJson(
|
||||
datosMaquetacion != null ? new ObjectMapper().writeValueAsString(datosMaquetacion) : null);
|
||||
presupuesto.setDatosMarcapaginasJson(
|
||||
datosMarcapaginas != null ? new ObjectMapper().writeValueAsString(datosMarcapaginas) : null);
|
||||
var resumen = this.getTextosResumen(presupuesto, serviciosList, datosMaquetacion, datosMarcapaginas,
|
||||
locale);
|
||||
|
||||
Object serviciosObj = resumen.get("servicios");
|
||||
|
||||
if (serviciosObj instanceof List<?> servicios && !servicios.isEmpty()) {
|
||||
// serializa a JSON válido
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
String json = objectMapper.writeValueAsString(servicios);
|
||||
presupuesto.setServiciosJson(json);
|
||||
} else {
|
||||
// decide tu política: null o "[]"
|
||||
presupuesto.setServiciosJson(null); // o presupuesto.setServiciosJson("[]");
|
||||
}
|
||||
|
||||
if (cliente_id != null && !mode.equals("public")) {
|
||||
|
||||
presupuesto.setUser(userRepo.findById(cliente_id).orElse(null));
|
||||
presupuesto.setOrigen(Presupuesto.Origen.privado);
|
||||
if (id != null) {
|
||||
presupuesto.setId(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (mode.equals("public")) {
|
||||
presupuesto.setOrigen(Presupuesto.Origen.publico);
|
||||
String sessionId = request.getSession(true).getId();
|
||||
String ip = IpUtils.getClientIp(request);
|
||||
|
||||
presupuesto = this.getDatosLocalizacion(presupuesto, sessionId, ip);
|
||||
if (id != null) {
|
||||
presupuesto.setId(id); // para que actualice, no cree uno nuevo
|
||||
}
|
||||
}
|
||||
presupuesto = this.generateTotalizadores(presupuesto, serviciosList, resumen, locale);
|
||||
|
||||
presupuestoRepository.saveAndFlush(presupuesto);
|
||||
|
||||
result.put("success", true);
|
||||
result.put("presupuesto_id", presupuesto.getId());
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
System.out.println("Error guardando presupuesto: " + e.getMessage());
|
||||
result.put("success", false);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* PRIVADO (futuro botón "Guardar"): persiste el presupuesto como borrador.
|
||||
*/
|
||||
|
||||
public HashMap<String, Object> calcularPresupuesto(Presupuesto presupuesto, Locale locale) {
|
||||
HashMap<String, Object> price = new HashMap<>();
|
||||
String priceStr = apiClient.getPrice(this.toSkApiRequest(presupuesto), presupuesto.getTipoEncuadernacion(), presupuesto.getTipoCubierta());
|
||||
String priceStr = apiClient.getPrice(this.toSkApiRequest(presupuesto), presupuesto.getTipoEncuadernacion(),
|
||||
presupuesto.getTipoCubierta());
|
||||
|
||||
try {
|
||||
price = new ObjectMapper().readValue(priceStr, new TypeReference<>() {
|
||||
@ -855,7 +1223,145 @@ public class PresupuestoService {
|
||||
price = new HashMap<>();
|
||||
price.put("error", messageSource.getMessage("presupuesto.error-obtener-precio", null, locale));
|
||||
}
|
||||
|
||||
return price;
|
||||
}
|
||||
|
||||
public Boolean canAccessPresupuesto(Presupuesto presupuesto, Authentication authentication) {
|
||||
|
||||
boolean isUser = authentication.getAuthorities().stream()
|
||||
.anyMatch(a -> a.getAuthority().equals("ROLE_USER"));
|
||||
|
||||
if (isUser) {
|
||||
// Si es usuario, solo puede ver sus propios presupuestos
|
||||
String username = authentication.getName();
|
||||
if (presupuesto.getUser() == null || !presupuesto.getUser().getUserName().equals(username)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// Métodos privados
|
||||
// =======================================================================
|
||||
/**
|
||||
* Copia de campos "actualizables" para no machacar otros (created_at, etc.)
|
||||
*/
|
||||
private Presupuesto mergePresupuesto(Presupuesto target, Presupuesto src) {
|
||||
// Campos funcionales
|
||||
target.setTitulo(src.getTitulo());
|
||||
target.setTipoEncuadernacion(src.getTipoEncuadernacion());
|
||||
target.setTipoCubierta(src.getTipoCubierta());
|
||||
target.setTipoImpresion(src.getTipoImpresion());
|
||||
target.setPaginasNegro(src.getPaginasNegro());
|
||||
target.setPaginasColor(src.getPaginasColor());
|
||||
target.setPaginasColorTotal(src.getPaginasColorTotal());
|
||||
target.setPosicionPaginasColor(src.getPosicionPaginasColor());
|
||||
target.setAncho(src.getAncho());
|
||||
target.setAlto(src.getAlto());
|
||||
target.setPapelInteriorId(src.getPapelInteriorId());
|
||||
target.setGramajeInterior(src.getGramajeInterior());
|
||||
target.setPapelCubiertaId(src.getPapelCubiertaId());
|
||||
target.setGramajeCubierta(src.getGramajeCubierta());
|
||||
target.setCubiertaCaras(src.getCubiertaCaras());
|
||||
target.setSolapasCubierta(src.getSolapasCubierta());
|
||||
target.setTamanioSolapasCubierta(src.getTamanioSolapasCubierta());
|
||||
target.setSobrecubierta(src.getSobrecubierta());
|
||||
target.setPapelSobrecubiertaId(src.getPapelSobrecubiertaId());
|
||||
target.setGramajeSobrecubierta(src.getGramajeSobrecubierta());
|
||||
target.setTamanioSolapasSobrecubierta(src.getTamanioSolapasSobrecubierta());
|
||||
target.setAcabado(src.getAcabado());
|
||||
target.setCabezada(src.getCabezada());
|
||||
target.setTipoCubierta(src.getTipoCubierta());
|
||||
target.setSelectedTirada(src.getSelectedTirada());
|
||||
target.setTirada1(src.getTirada1());
|
||||
target.setTirada2(src.getTirada2());
|
||||
target.setTirada3(src.getTirada3());
|
||||
target.setTirada4(src.getTirada4());
|
||||
|
||||
// Metadatos y totales
|
||||
target.setEstado(Presupuesto.Estado.borrador);
|
||||
target.setOrigen(src.getOrigen());
|
||||
target.setSessionId(src.getSessionId());
|
||||
target.setIpHash(src.getIpHash());
|
||||
target.setIpTrunc(src.getIpTrunc());
|
||||
target.setPais(src.getPais());
|
||||
target.setRegion(src.getRegion());
|
||||
target.setCiudad(src.getCiudad());
|
||||
target.setServiciosJson(src.getServiciosJson());
|
||||
target.setPreciosPorTiradaJson(src.getPreciosPorTiradaJson());
|
||||
target.setPrecioUnitario(src.getPrecioUnitario());
|
||||
target.setPrecioTotalTirada(src.getPrecioTotalTirada());
|
||||
target.setServiciosTotal(src.getServiciosTotal());
|
||||
target.setBaseImponible(src.getBaseImponible());
|
||||
target.setIvaReducido(src.getIvaReducido());
|
||||
target.setEntregaTipo(src.getEntregaTipo());
|
||||
target.setIvaImporte4(src.getIvaImporte4());
|
||||
target.setIvaImporte21(src.getIvaImporte21());
|
||||
target.setTotalConIva(src.getTotalConIva());
|
||||
target.setCreatedBy(target.getCreatedBy() == null ? src.getCreatedBy() : target.getCreatedBy()); // no pisar si
|
||||
// ya existe
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
private Double obtenerPrototipo(Presupuesto presupuesto) {
|
||||
// Obtenemos el precio de 1 unidad para el ejemplar de prueba
|
||||
HashMap<String, Object> price = new HashMap<>();
|
||||
Presupuesto presupuestoTemp = presupuesto.clone();
|
||||
presupuestoTemp.setTirada1(1);
|
||||
presupuestoTemp.setTirada2(null);
|
||||
presupuestoTemp.setTirada3(null);
|
||||
presupuestoTemp.setTirada4(null);
|
||||
if (presupuestoTemp.getTipoImpresion() == Presupuesto.TipoImpresion.color) {
|
||||
presupuestoTemp.setTipoImpresion(Presupuesto.TipoImpresion.colorhq);
|
||||
} else if (presupuestoTemp.getTipoImpresion() == Presupuesto.TipoImpresion.negro) {
|
||||
presupuestoTemp.setTipoImpresion(Presupuesto.TipoImpresion.negrohq);
|
||||
}
|
||||
String priceStr = apiClient.getPrice(this.toSkApiRequest(presupuestoTemp),
|
||||
presupuestoTemp.getTipoEncuadernacion(), presupuestoTemp.getTipoCubierta());
|
||||
Double price_prototipo = 0.0;
|
||||
try {
|
||||
price = new ObjectMapper().readValue(priceStr, new TypeReference<>() {
|
||||
});
|
||||
price_prototipo = ((List<Double>) ((Map<String, Object>) price.get("data")).get("precios")).get(0);
|
||||
if (price_prototipo < 25) {
|
||||
price_prototipo = 25.0;
|
||||
}
|
||||
} catch (JsonProcessingException e) {
|
||||
} catch (Exception exception) {
|
||||
}
|
||||
return price_prototipo;
|
||||
}
|
||||
|
||||
// Utilidad local (puedes moverla a una clase Utils si quieres)
|
||||
private static String anonymizeIp(String ip) {
|
||||
if (ip == null)
|
||||
return null;
|
||||
// IPv4
|
||||
if (ip.contains(".") && !ip.contains(":")) {
|
||||
String[] p = ip.split("\\.");
|
||||
if (p.length == 4) {
|
||||
return p[0] + "." + p[1] + "." + p[2] + ".0";
|
||||
}
|
||||
}
|
||||
// IPv6: quedarnos con /64 -> primera mitad y rellenar
|
||||
if (ip.contains(":")) {
|
||||
String[] parts = ip.split(":", -1);
|
||||
// expand no estricta, nos quedamos con primeros 4 bloques y completamos
|
||||
int blocks = Math.min(parts.length, 4);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < blocks; i++) {
|
||||
if (i > 0)
|
||||
sb.append(":");
|
||||
sb.append(parts[i].isEmpty() ? "0" : parts[i]);
|
||||
}
|
||||
// completar a /64 con ceros
|
||||
for (int i = blocks; i < 8; i++)
|
||||
sb.append(":0");
|
||||
return sb.toString();
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
package com.imprimelibros.erp.presupuesto.validation;
|
||||
|
||||
import com.imprimelibros.erp.configurationERP.VariableService;
|
||||
import com.imprimelibros.erp.presupuesto.Presupuesto;
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||
|
||||
import jakarta.validation.ConstraintValidator;
|
||||
import jakarta.validation.ConstraintValidatorContext;
|
||||
|
||||
@ -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 = PaginasCosidoGrapadoValidator.class)
|
||||
@Target({ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface PaginasCosidoGrapado {
|
||||
String message() default "Las tiradas deben ser todas mayores o todas menores al valor POD";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
package com.imprimelibros.erp.presupuesto.validation;
|
||||
|
||||
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 PaginasCosidoGrapadoValidator implements ConstraintValidator<PaginasCosidoGrapado, 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 ||
|
||||
presupuesto.getTipoEncuadernacion() == Presupuesto.TipoEncuadernacion.grapado)) {
|
||||
if (presupuesto.getPaginasColor() > 0 && presupuesto.getPaginasNegro() > 0) {
|
||||
String mensajeInterpolado = messageSource.getMessage(
|
||||
"presupuesto.errores.tipo-paginas-cosido-grapado",
|
||||
null,
|
||||
LocaleContextHolder.getLocale() // respeta el idioma actual
|
||||
);
|
||||
context.disableDefaultConstraintViolation();
|
||||
context.buildConstraintViolationWithTemplate(mensajeInterpolado)
|
||||
.addConstraintViolation();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,14 @@
|
||||
package com.imprimelibros.erp.presupuesto.validation;
|
||||
|
||||
import jakarta.validation.GroupSequence;
|
||||
|
||||
public class PresupuestoValidationGroups {
|
||||
|
||||
public interface DatosGenerales {}
|
||||
public interface Interior {}
|
||||
public interface Cubierta {}
|
||||
|
||||
@GroupSequence({DatosGenerales.class, Interior.class, Cubierta.class})
|
||||
public interface All {}
|
||||
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package com.imprimelibros.erp.presupuesto.validation;
|
||||
|
||||
import com.imprimelibros.erp.configurationERP.VariableService;
|
||||
import com.imprimelibros.erp.presupuesto.Presupuesto;
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||
|
||||
import jakarta.validation.ConstraintValidator;
|
||||
import jakarta.validation.ConstraintValidatorContext;
|
||||
|
||||
@ -7,7 +7,6 @@ import java.lang.reflect.Method;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.EntityManagerFactory;
|
||||
import jakarta.persistence.FlushModeType;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
import jakarta.persistence.PersistenceUnit;
|
||||
import jakarta.persistence.criteria.*;
|
||||
import jakarta.validation.ConstraintValidator;
|
||||
|
||||
@ -255,7 +255,6 @@ public class User {
|
||||
", fullName='" + fullName + '\'' +
|
||||
", userName='" + userName + '\'' +
|
||||
", enabled=" + enabled +
|
||||
", roles=" + getRoles() +
|
||||
'}';
|
||||
}
|
||||
|
||||
|
||||
@ -8,8 +8,10 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@ -25,6 +27,9 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
||||
import com.imprimelibros.erp.datatables.DataTablesRequest;
|
||||
import com.imprimelibros.erp.datatables.DataTablesParser;
|
||||
import com.imprimelibros.erp.config.Sanitizer;
|
||||
@ -34,6 +39,7 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
@ -52,6 +58,7 @@ public class UserController {
|
||||
private Sanitizer sanitizer;
|
||||
private PasswordEncoder passwordEncoder;
|
||||
private TranslationService translationService;
|
||||
private UserService userService;
|
||||
|
||||
public UserController(UserDao repo, UserService userService, MessageSource messageSource, Sanitizer sanitizer,
|
||||
PasswordEncoder passwordEncoder, RoleDao roleRepo, TranslationService translationService) {
|
||||
@ -61,6 +68,7 @@ public class UserController {
|
||||
this.roleRepo = roleRepo;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.translationService = translationService;
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@ -346,4 +354,35 @@ public class UserController {
|
||||
}).orElseGet(() -> ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body(Map.of("message", messageSource.getMessage("usuarios.error.not-found", null, locale))));
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
@GetMapping(value = "api/get-users", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public Map<String, Object> getUsers(
|
||||
@RequestParam(required = false) String role, // puede venir ausente
|
||||
@RequestParam(required = false) String q,
|
||||
@RequestParam(defaultValue = "1") int page,
|
||||
@RequestParam(defaultValue = "10") int size) {
|
||||
|
||||
Pageable pageable = PageRequest.of(Math.max(0, page - 1), size);
|
||||
|
||||
Page<User> users = userService.findByRoleAndSearch(role, q, pageable);
|
||||
|
||||
boolean more = users.hasNext();
|
||||
|
||||
List<Map<String, Object>> results = users.getContent().stream()
|
||||
.map(u -> {
|
||||
Map<String, Object> m = new HashMap<>();
|
||||
m.put("id", u.getId());
|
||||
m.put("text", (u.getFullName() != null && !u.getFullName().isBlank())
|
||||
? u.getFullName()
|
||||
: u.getUserName());
|
||||
return m;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return Map.of(
|
||||
"results", results,
|
||||
"pagination", Map.of("more", more));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,39 +19,60 @@ import org.springframework.lang.Nullable;
|
||||
@Repository
|
||||
public interface UserDao extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
|
||||
|
||||
// Aplicamos EntityGraph a la versión con Specification+Pageable
|
||||
@Override
|
||||
@EntityGraph(attributePaths = { "rolesLink", "rolesLink.role" })
|
||||
@NonNull
|
||||
Page<User> findAll(@Nullable Specification<User> spec, @NonNull Pageable pageable);
|
||||
// Aplicamos EntityGraph a la versión con Specification+Pageable
|
||||
@Override
|
||||
@EntityGraph(attributePaths = { "rolesLink", "rolesLink.role" })
|
||||
@NonNull
|
||||
Page<User> findAll(@Nullable Specification<User> spec, @NonNull Pageable pageable);
|
||||
|
||||
Optional<User> findByUserNameIgnoreCase(String userName);
|
||||
Optional<User> findByUserNameIgnoreCase(String userName);
|
||||
|
||||
boolean existsByUserNameIgnoreCase(String userName);
|
||||
boolean existsByUserNameIgnoreCase(String userName);
|
||||
|
||||
// Para comprobar si existe al hacer signup
|
||||
@Query(value = """
|
||||
SELECT id, deleted, enabled
|
||||
FROM users
|
||||
WHERE LOWER(username) = LOWER(:userName)
|
||||
LIMIT 1
|
||||
""", nativeQuery = true)
|
||||
Optional<UserLite> findLiteByUserNameIgnoreCase(@Param("userName") String userName);
|
||||
// Para comprobar si existe al hacer signup
|
||||
@Query(value = """
|
||||
SELECT id, deleted, enabled
|
||||
FROM users
|
||||
WHERE LOWER(username) = LOWER(:userName)
|
||||
LIMIT 1
|
||||
""", nativeQuery = true)
|
||||
Optional<UserLite> findLiteByUserNameIgnoreCase(@Param("userName") String userName);
|
||||
|
||||
boolean existsByUserNameIgnoreCaseAndIdNot(String userName, Long id);
|
||||
boolean existsByUserNameIgnoreCaseAndIdNot(String userName, Long id);
|
||||
|
||||
// Nuevo: para login/negocio "activo"
|
||||
@EntityGraph(attributePaths = { "rolesLink", "rolesLink.role" })
|
||||
Optional<User> findByUserNameIgnoreCaseAndEnabledTrueAndDeletedFalse(String userName);
|
||||
// Nuevo: para login/negocio "activo"
|
||||
@EntityGraph(attributePaths = { "rolesLink", "rolesLink.role" })
|
||||
Optional<User> findByUserNameIgnoreCaseAndEnabledTrueAndDeletedFalse(String userName);
|
||||
|
||||
// Para poder restaurar, necesitas leer ignorando @Where (native):
|
||||
@Query(value = "SELECT * FROM users WHERE id = :id", nativeQuery = true)
|
||||
Optional<User> findByIdIncludingDeleted(@Param("id") Long id);
|
||||
// Para poder restaurar, necesitas leer ignorando @Where (native):
|
||||
@Query(value = "SELECT * FROM users WHERE id = :id", nativeQuery = true)
|
||||
Optional<User> findByIdIncludingDeleted(@Param("id") Long id);
|
||||
|
||||
@Query(value = "SELECT * FROM users WHERE deleted = TRUE", nativeQuery = true)
|
||||
List<User> findAllDeleted();
|
||||
@Query(value = "SELECT * FROM users WHERE deleted = TRUE", nativeQuery = true)
|
||||
List<User> findAllDeleted();
|
||||
|
||||
@Query("select u.id from User u where lower(u.userName) = lower(:userName)")
|
||||
Optional<Long> findIdByUserNameIgnoreCase(@Param("userName") String userName);
|
||||
@Query("select u.id from User u where lower(u.userName) = lower(:userName)")
|
||||
Optional<Long> findIdByUserNameIgnoreCase(@Param("userName") String userName);
|
||||
|
||||
@Query(value = """
|
||||
SELECT DISTINCT u
|
||||
FROM User u
|
||||
JOIN u.rolesLink rl
|
||||
JOIN rl.role r
|
||||
WHERE (:role IS NULL OR r.name = :role)
|
||||
AND (:q IS NULL OR LOWER(u.fullName) LIKE LOWER(CONCAT('%', :q, '%'))
|
||||
OR LOWER(u.userName) LIKE LOWER(CONCAT('%', :q, '%')))
|
||||
""", countQuery = """
|
||||
SELECT COUNT(DISTINCT u.id)
|
||||
FROM User u
|
||||
JOIN u.rolesLink rl
|
||||
JOIN rl.role r
|
||||
WHERE (:role IS NULL OR r.name = :role)
|
||||
AND (:q IS NULL OR LOWER(u.fullName) LIKE LOWER(CONCAT('%', :q, '%'))
|
||||
OR LOWER(u.userName) LIKE LOWER(CONCAT('%', :q, '%')))
|
||||
""")
|
||||
Page<User> searchUsers(@Param("role") String role,
|
||||
@Param("q") String q,
|
||||
Pageable pageable);
|
||||
|
||||
}
|
||||
|
||||
@ -1,7 +1,19 @@
|
||||
package com.imprimelibros.erp.users;
|
||||
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
||||
public interface UserService extends UserDetailsService {
|
||||
|
||||
|
||||
/**
|
||||
* Busca usuarios por rol y texto libre (en username, email o fullName),
|
||||
* con paginación.
|
||||
*
|
||||
* @param role nombre del rol (ej. "ROL_USER")
|
||||
* @param query texto de búsqueda (puede ser null o vacío)
|
||||
* @param pageable paginación
|
||||
* @return página de usuarios
|
||||
*/
|
||||
Page<User> findByRoleAndSearch(String role, String query, Pageable pageable);
|
||||
}
|
||||
|
||||
@ -2,6 +2,8 @@ package com.imprimelibros.erp.users;
|
||||
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@ -13,10 +15,19 @@ public class UserServiceImpl implements UserService {
|
||||
this.userDao = userDao;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) {
|
||||
User user = userDao.findByUserNameIgnoreCaseAndEnabledTrueAndDeletedFalse(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("No existe usuario activo: " + username));
|
||||
return new UserDetailsImpl(user);
|
||||
}
|
||||
|
||||
// ===== Búsqueda para Select2 =====
|
||||
@Override
|
||||
public Page<User> findByRoleAndSearch(String role, String query, Pageable pageable) {
|
||||
|
||||
if (query == null || query.isBlank()) query = null;
|
||||
return userDao.searchUsers(role, query, pageable);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -3,8 +3,8 @@ spring.application.name=erp
|
||||
#
|
||||
# Logging
|
||||
#
|
||||
logging.level.org.springframework.security=DEBUG
|
||||
logging.level.root=WARN
|
||||
logging.level.org.springframework.security=ERROR
|
||||
logging.level.root=ERROR
|
||||
logging.level.org.springframework=ERROR
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ spring.datasource.password=om91irrDctd
|
||||
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
|
||||
|
||||
#spring.jpa.hibernate.ddl-auto=update
|
||||
spring.jpa.show-sql=true
|
||||
spring.jpa.show-sql=false
|
||||
|
||||
|
||||
#
|
||||
@ -33,12 +33,10 @@ safekat.api.password=Safekat2024
|
||||
#
|
||||
# Debug JPA / Hibernate
|
||||
#
|
||||
#spring.jpa.show-sql=true
|
||||
#logging.level.org.hibernate.SQL=DEBUG
|
||||
#logging.level.org.hibernate.orm.jdbc.bind=TRACE
|
||||
#spring.jpa.properties.hibernate.format_sql=true
|
||||
|
||||
|
||||
#
|
||||
# Resource chain
|
||||
# Activa el resource chain y versionado por contenido
|
||||
@ -73,4 +71,24 @@ spring.mail.properties.mail.smtp.starttls.enable=true
|
||||
#
|
||||
# Remove JSESSIONID from URL
|
||||
#
|
||||
server.servlet.session.persistent=false
|
||||
server.servlet.session.persistent=false
|
||||
|
||||
#
|
||||
# GeoIP
|
||||
#
|
||||
geoip.enabled=true
|
||||
geoip.maxmind.enabled=true
|
||||
geoip.http.enabled=true
|
||||
|
||||
|
||||
#
|
||||
# Hibernate Timezone
|
||||
#
|
||||
spring.jpa.properties.hibernate.jdbc.time_zone=UTC
|
||||
|
||||
#
|
||||
# PDF Templates
|
||||
#
|
||||
# PDF Templates
|
||||
imprimelibros.pdf.templates.PRESUPUESTO_presupuesto-a4=imprimelibros/pdf/presupuesto-a4
|
||||
imprimelibros.pdf.templates.FACTURA_factura-a4=imprimelibros/pdf/factura-a4
|
||||
|
||||
BIN
src/main/resources/geoip/GeoLite2-City.mmdb
Normal file
BIN
src/main/resources/geoip/GeoLite2-City.mmdb
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 59 MiB |
@ -7,6 +7,8 @@ app.guardar=Guardar
|
||||
app.editar=Editar
|
||||
app.eliminar=Eliminar
|
||||
app.imprimir=Imprimir
|
||||
app.acciones.siguiente=Siguiente
|
||||
app.acciones.anterior=Anterior
|
||||
|
||||
app.bienvenido=Bienvenido
|
||||
app.perfil=Perfil
|
||||
@ -14,5 +16,8 @@ app.mensajes=Mensajes
|
||||
app.logout=Cerrar sesión
|
||||
|
||||
app.sidebar.inicio=Inicio
|
||||
app.sidebar.presupuestos=Presupuestos
|
||||
app.sidebar.configuracion=Configuración
|
||||
app.sidebar.usuarios=Usuarios
|
||||
app.sidebar.configuracion=Configuración
|
||||
|
||||
app.errors.403=No tienes permiso para acceder a esta página.
|
||||
1
src/main/resources/i18n/cart_en.properties
Normal file
1
src/main/resources/i18n/cart_en.properties
Normal file
@ -0,0 +1 @@
|
||||
|
||||
14
src/main/resources/i18n/cart_es.properties
Normal file
14
src/main/resources/i18n/cart_es.properties
Normal file
@ -0,0 +1,14 @@
|
||||
cart.title=Cesta de la compra
|
||||
cart.empty=Tu cesta de la compra está vacía.
|
||||
|
||||
cart.item.presupuesto-numero=Presupuesto #
|
||||
cart.precio=Precio
|
||||
|
||||
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.tramitar=Tramitar pedido
|
||||
|
||||
cart.resumen.fidelizacion=Si tiene descuento por fidelización, se aplicará al tramitar el pedido.
|
||||
0
src/main/resources/i18n/pdf_en.properties
Normal file
0
src/main/resources/i18n/pdf_en.properties
Normal file
34
src/main/resources/i18n/pdf_es.properties
Normal file
34
src/main/resources/i18n/pdf_es.properties
Normal file
@ -0,0 +1,34 @@
|
||||
pdf.company.name=Impresión ImprimeLibros S.L.
|
||||
pdf.company.address=C/ José Picón, 28 local A
|
||||
pdf.company.postalcode=28028
|
||||
pdf.company.city=Madrid
|
||||
pdf.company.phone=+34 910052574
|
||||
|
||||
pdf.presupuesto=PRESUPUESTO
|
||||
pdf.factura=FACTURA
|
||||
pdf.pedido=PEDIDO
|
||||
|
||||
# Presupuesto
|
||||
pdf.presupuesto.number=PRESUPUESTO Nº:
|
||||
pdf.presupuesto.client=CLIENTE:
|
||||
pdf.presupuesto.date=FECHA:
|
||||
|
||||
pdf.presupuesto.titulo=Título:
|
||||
pdf.table.tirada=TIRADA
|
||||
pdf.table.impresion=IMPRESIÓN
|
||||
pdf.table.servicios=SERVICIOS
|
||||
pdf.table.iva-4=IVA 4%
|
||||
pdf.table.iva-21=IVA 21%
|
||||
pdf.table.precio-total=PRECIO TOTAL
|
||||
|
||||
pdf.servicios-adicionales=Servicios adicionales:
|
||||
pdf.ejemplares-deposito-legal=Impresión de {0} ejemplares de depósito legal<br />
|
||||
pdf.datos-maquetacion=Datos de maquetación:
|
||||
pdf.datos-marcapaginas=Datos de marcapáginas:
|
||||
|
||||
pdf.incluye-envio=El presupuesto incluye el envío a una dirección de la península.
|
||||
|
||||
pdf.politica-privacidad=Política de privacidad
|
||||
pdf.politica-privacidad.responsable=Responsable: Impresión Imprime Libros - CIF: B04998886 - Teléfono de contacto: 910052574
|
||||
pdf.politica-privacidad.correo-direccion=Correo electrónico: info@imprimelibros.com - Dirección postal: Calle José Picón, Nº 28 Local A, 28028, Madrid
|
||||
pdf.politica-privacidad.aviso=Le comunicamos que los datos que usted nos facilite quedarán incorporados en nuestro registro interno de actividades de tratamiento con el fin de llevar a cabo una adecuada gestión fiscal y contable. Los datos proporcionados se conservarán mientras se mantenga la relación comercial o durante los años necesarios para cumplir con las obligaciones legales. Así mismo, los datos no serán cedidos a terceros salvo en aquellos casos en que exista una obligación legal. Tiene derecho a acceder a sus datos personales, rectificar los datos inexactos, solicitar su supresión, limitar alguno de los tratamientos u oponerse a algún uso vía e-mail, personalmente o mediante correo postal.
|
||||
@ -1,3 +1,6 @@
|
||||
presupuesto.title=Presupuestos
|
||||
presupuesto.editar.title=Editar presupuesto
|
||||
presupuesto.eliminar=Eliminar presupuesto
|
||||
presupuesto.datos-generales=Datos Generales
|
||||
presupuesto.interior=Interior
|
||||
presupuesto.cubierta=Cubierta
|
||||
@ -6,6 +9,33 @@ presupuesto.extras=Extras
|
||||
presupuesto.resumen=Resumen
|
||||
presupuesto.add-to-presupuesto=Añadir al presupuesto
|
||||
presupuesto.calcular=Calcular
|
||||
presupuesto.add=Añadir presupuesto
|
||||
presupuesto.guardar=Guardar
|
||||
presupuesto.add-to-cart=Añadir a la cesta
|
||||
|
||||
presupuesto.nav.presupuestos-cliente=Presupuestos cliente
|
||||
presupuesto.nav.presupuestos-anonimos=Presupuestos anónimos
|
||||
|
||||
presupuesto.estado.borrador=Borrador
|
||||
presupuesto.estado.aceptado=Aceptado
|
||||
presupuesto.estado.modificado=Modificado
|
||||
|
||||
presupuesto.tabla.id=ID
|
||||
presupuesto.tabla.numero=Número
|
||||
presupuesto.tabla.titulo=Título
|
||||
presupuesto.tabla.cliente=Cliente
|
||||
presupuesto.tabla.encuadernacion=Encuadernación
|
||||
presupuesto.tabla.cubierta=Cubierta
|
||||
presupuesto.tabla.tipo-impresion=Tipo de impresión
|
||||
presupuesto.tabla.tirada=Tirada
|
||||
presupuesto.tabla.paginas=Páginas
|
||||
presupuesto.tabla.estado=Estado
|
||||
presupuesto.tabla.total-iva=Total (con IVA)
|
||||
presupuesto.tabla.updated-at=Actualizado el
|
||||
presupuesto.tabla.pais=País
|
||||
presupuesto.tabla.region=Región
|
||||
presupuesto.tabla.ciudad=Ciudad
|
||||
presupuesto.tabla.acciones=Acciones
|
||||
|
||||
# Pestaña datos generales de presupuesto
|
||||
presupuesto.informacion-libro=Información del libro
|
||||
@ -44,6 +74,15 @@ presupuesto.espiral-descripcion=Espiral (a partir de 20 páginas)
|
||||
presupuesto.wire-o=Wire-O
|
||||
presupuesto.wireo=Wire-O
|
||||
presupuesto.wire-o-descripcion=Wire-O (a partir de 20 páginas)
|
||||
presupuesto.informacion-adicional=Información adicional
|
||||
presupuesto.informacion-adicional-descripcion=Datos adicionales
|
||||
presupuesto.iva-reducido=I.V.A reducido
|
||||
presupuesto.iva-reducido-descripcion=Se verificará que el pedido cumpla con los requisitos establecidos en el Artículo 91 de la Ley 37/1992, sobre inserción de publicidad, antes de proceder con su producción, lo que garantiza la aplicación del IVA reducido del 4%.
|
||||
presupuesto.entrega=Entrega
|
||||
presupuesto.entrega.peninsula=Península y Baleares
|
||||
presupuesto.entrega.canarias=Canarias
|
||||
presupuesto.entrega.paises-ue=Países UE
|
||||
|
||||
presupuesto.encuadernacion-descripcion=Seleccione la encuadernación del libro
|
||||
presupuesto.continuar-interior=Continuar a diseño interior
|
||||
|
||||
@ -63,8 +102,8 @@ presupuesto.offset-blanco-volumen=Offset Blanco Volumen
|
||||
presupuesto.offset-ahuesado=Offset Ahuesado
|
||||
presupuesto.offset-ahuesado-volumen=Offset Ahuesado Volumen
|
||||
presupuesto.estucado-mate=Estucado Mate
|
||||
presupuesto.volver-datos-generales=Volver a datos generales
|
||||
presupuesto.continuar-cubierta=Continuar a diseño cubierta
|
||||
presupuesto.volver-datos-generales=Datos generales
|
||||
presupuesto.continuar-cubierta=Diseño cubierta
|
||||
|
||||
# Pestaña cubierta
|
||||
presupuesto.plantilla-cubierta=Plantilla de cubierta
|
||||
@ -102,8 +141,8 @@ presupuesto.cartulina-grafica=Cartulina gráfica
|
||||
presupuesto.estucado-mate-cubierta=Estucado mate
|
||||
presupuesto.gramaje-cubierta=Gramaje cubierta
|
||||
presupuesto.gramaje-cubierta-descripcion=Seleccione el gramaje para la cubierta
|
||||
presupuesto.volver-interior=Volver a diseño interior
|
||||
presupuesto.continuar-seleccion-tirada=Continuar a selección de tirada
|
||||
presupuesto.volver-interior=Diseño interior
|
||||
presupuesto.continuar-seleccion-tirada=Selección de tirada
|
||||
presupuesto.offset=Offset
|
||||
presupuesto.estucado=Estucado
|
||||
presupuesto.verjurado=Verjurado
|
||||
@ -139,8 +178,8 @@ presupuesto.precio-unidad=Precio por unidad
|
||||
presupuesto.seleccionar-tirada=Seleccionar tirada
|
||||
presupuesto.tirada-seleccionada=Seleccionada
|
||||
presupuesto.unidades=UNIDADES
|
||||
presupuesto.volver-seleccion-tirada=Volver a selección de tirada
|
||||
presupuesto.continuar-extras-libro=Continuar a extras del libro
|
||||
presupuesto.volver-seleccion-tirada=Selección de tirada
|
||||
presupuesto.continuar-extras-libro=Extras del libro
|
||||
presupuesto.error-obtener-precio=No se pudo obtener el precio para los datos introducidos. Por favor, contacte con el soporte técnico.
|
||||
|
||||
#pestaña extras del libro
|
||||
@ -148,6 +187,7 @@ presupuesto.extras=Servicios Extras
|
||||
presupuesto.extras-descripcion=Seleccione los servicios adicionales que desea añadir al presupuesto
|
||||
presupuesto.extras-retractilado=Retractilado
|
||||
presupuesto.extras-isbn=ISBN
|
||||
presupuesto.extras-service-isbn=ISBN
|
||||
presupuesto.extras-deposito-legal=Depósito Legal
|
||||
presupuesto.extras-deposito-legal-descripcion=Se añadirán 4 ejemplares a la tirada
|
||||
presupuesto.extras-revision-archivos=Revisión de archivos
|
||||
@ -158,32 +198,34 @@ presupuesto.extras-marcapaginas=Marcapáginas
|
||||
presupuesto.extras-maquetacion=Maquetación
|
||||
presupuesto.extras-ferro-digital-ribbon=Incluido
|
||||
presupuesto.extras-calcular=Calcular
|
||||
presupuesto.volver-cubierta=Volver a diseño cubierta
|
||||
presupuesto.volver-cubierta=Diseño cubierta
|
||||
presupuesto.finalizar=Finalizar presupuesto
|
||||
presupuesto.calcular-presupuesto=Calcular presupuesto
|
||||
presupuesto.consultar-soporte=Consultar con soporte
|
||||
|
||||
# Pestaña resumen del presupuesto
|
||||
presupuesto.resumen.tamanio=Tamaño
|
||||
presupuesto.resumen.tabla.descripcion=Descripción
|
||||
presupuesto.resumen.tabla.cantidad=Cantidad
|
||||
presupuesto.resumen.tabla.precio-unidad=Precio/unidad
|
||||
presupuesto.resumen.tabla.precio-total=Precio total
|
||||
presupuesto.resumen.tabla.base=Base
|
||||
presupuesto.resumen.tabla.iva=I.V.A. (4%)
|
||||
presupuesto.resumen.tabla.iva4=I.V.A. (4%)
|
||||
presupuesto.resumen.tabla.iva21=I.V.A. (21%)
|
||||
presupuesto.resumen.tabla.total=Total presupuesto
|
||||
presupuesto.resumen-texto=Impresion de {0} unidades encuadernadas en {1} en {2} con {3} páginas en formato {4} x {5} mm. \
|
||||
presupuesto.resumen-texto=Impresion de {0} ejemplares con {3} páginas, tamaño {4}x{5} mm e impresas en {2} con encuadernación {1}. \
|
||||
<ul> \
|
||||
<li>Papel interior {6} {7} gr.</li> \
|
||||
<li>Cubierta {8} en {9} {10} gr.</li>
|
||||
presupuesto.resumen-texto-impresion-caras-cubierta=<li>Impresa a {0}.</li>
|
||||
presupuesto.resumen-texto-solapas-cubierta=<li>Solapas de {0} mm.</li>
|
||||
presupuesto.resumen-texto-guardas-cabezada= <li>Guardas {0} en {1} {2}. Color de la cabezada: {3}.</li>
|
||||
presupuesto.resumen-texto-acabado-cubierta= <li>Acabado {0}. </li>
|
||||
presupuesto.resumen-texto-acabado-cubierta= <li>Acabado: {0}. </li>
|
||||
presupuesto.resumen-texto-end=</ul>
|
||||
presupuesto.resumen-texto-sobrecubierta=<li>Sobrecubierta impresa en {0} {1} gr. <ul><li>Acabado {2}</li><li>Solapas: {3} mm.</li></ul></li>
|
||||
presupuesto.resumen-texto-faja=<li>Faja impresa en {0} {1} gr. con un alto de {2} mm. <ul><li>Acabado {3}</li><li>Solapas: {4} mm.</li></ul></li>
|
||||
presupuesto.resumen-texto-sobrecubierta=<li>Sobrecubierta impresa en {0} {1} gr. <ul><li>Acabado: {2}</li><li>Solapas: {3} mm.</li></ul></li>
|
||||
presupuesto.resumen-texto-faja=<li>Faja impresa en {0} {1} gr. con un alto de {2} mm. <ul><li>Acabado: {3}</li><li>Solapas: {4} mm.</li></ul></li>
|
||||
presupuesto.resumen-deposito-legal=Ejemplares para el Depósito Legal
|
||||
presupuesto.volver-extras=Volver a extras
|
||||
presupuesto.volver-extras=Extras del libro
|
||||
presupuesto.resumen.inicie-sesion=Inicie sesión para continuar
|
||||
presupuesto.resumen.agregar-cesta=Agregar a la cesta
|
||||
|
||||
@ -201,6 +243,7 @@ presupuesto.papel-gramaje=Papel y gramaje
|
||||
# Presupuesto de maquetación
|
||||
presupuesto.maquetacion=Presupuesto de maquetación
|
||||
presupuesto.maquetacion.num-caracteres=Número de caracteres
|
||||
presupuesto.maquetacion.caracteres=caracteres
|
||||
presupuesto.maquetacion.num-caracteres-descripcion=Caracteres con espacios (obtenidos desde Word)
|
||||
presupuesto.maquetacion.formato=Formato
|
||||
presupuesto.maquetacion.formato-descripcion=Seleccione el tamaño que más se aproxime
|
||||
@ -239,6 +282,32 @@ presupuesto.marcapaginas.acabado.plastificado-mate-2c=Plastificado mate 2/C
|
||||
presupuesto.marcapaginas.precio-unidad=Precio por unidad
|
||||
presupuesto.marcapaginas.precio-total=Precio total
|
||||
|
||||
# Mensajes de eliminación de presupuesto
|
||||
presupuesto.delete.title=Eliminar presupuesto
|
||||
presupuesto.delete.button=Si, ELIMINAR
|
||||
presupuesto.delete.text=¿Está seguro de que desea eliminar este presupuesto?<br>Esta acción no se puede deshacer.
|
||||
presupuesto.delete.ok.title=Presupuesto eliminado
|
||||
presupuesto.delete.ok.text=El presupuesto ha sido eliminado con éxito.
|
||||
presupuesto.exito.eliminado=Presupuesto eliminado con éxito.
|
||||
presupuesto.error.delete-internal-error=No se puede eliminar: error interno.
|
||||
presupuesto.error.delete-permission-denied=No se puede eliminar: permiso denegado.
|
||||
presupuesto.error.delete-not-found=No se puede eliminar: presupuesto no encontrado.
|
||||
presupuesto.error.delete-not-draft=Solo se pueden eliminar presupuestos en estado Borrador.
|
||||
|
||||
# Añadir presupuesto
|
||||
presupuesto.add.tipo=Tipo de presupuesto
|
||||
presupuesto.add.anonimo=Anónimo
|
||||
presupuesto.add.cliente=De cliente
|
||||
presupuesto.add.next=Siguiente
|
||||
presupuesto.add.cancel=Cancelar
|
||||
presupuesto.add.select-client=Seleccione cliente
|
||||
presupuesto.add.error.options=Debe seleccionar una opción
|
||||
presupuesto.add.error.options-client=Debe seleccionar un cliente
|
||||
presupuesto.add.error.save.title=Error al guardar
|
||||
presupuesto.error.save-internal-error=No se puede guardar: error interno.
|
||||
presupuesto.exito.guardado=Presupuesto guardado con éxito.
|
||||
presupuesto.exito.guardado-actualizado=Presupuesto actualizado con éxito.
|
||||
|
||||
# Errores
|
||||
presupuesto.errores-title=Corrija los siguientes errores:
|
||||
presupuesto.errores.titulo=El título es obligatorio
|
||||
@ -248,6 +317,7 @@ presupuesto.errores.paginasNegro.required=El número de páginas en negro es obl
|
||||
presupuesto.errores.paginasNegro.par=El número de páginas en negro debe ser par
|
||||
presupuesto.errores.paginasColor.required=El número de páginas en color es obligatorio
|
||||
presupuesto.errores.paginasColor.par=El número de páginas en color debe ser par
|
||||
presupuesto.errores.tipo-paginas-cosido-grapado=Para encuadernación cosido o grapado, sólo se pueden seleccionar o bien páginas a color o páginas en blanco y negro. No se pueden mezclar.
|
||||
presupuesto.errores.tipo-encuadernacion=Seleccione el tipo de libro
|
||||
presupuesto.errores.ancho=El ancho no puede estar vacío
|
||||
presupuesto.errores.ancho.min_max=El ancho tiene que estar en el rango [{0}, {1}] mm;
|
||||
@ -261,6 +331,11 @@ presupuesto.errores.solapas-cubierta=Seleccione si desea o no solapas en la cubi
|
||||
presupuesto.errores.papel-cubierta=Seleccione el tipo de papel para la cubierta
|
||||
presupuesto.errores.gramaje-cubierta=Seleccione el gramaje del papel para la cubierta
|
||||
presupuesto.errores.acabado-cubierta=Seleccione el acabado de la cubierta
|
||||
presupuesto.errores.error-interior=Se ha producido un error al procesar el interior. Error {0}. Por favor, contacte con soporte.
|
||||
|
||||
presupuesto.errores.presupuesto-no-existe=El presupuesto con ID {0} no existe.
|
||||
|
||||
presupuesto.errores.presupuesto-maquetacion=No se pudo calcular el presupuesto de maquetación.
|
||||
presupuesto.errores.presupuesto-marcapaginas=No se pudo calcular el presupuesto de marcapáginas.
|
||||
presupuesto.errores.presupuesto-marcapaginas=No se pudo calcular el presupuesto de marcapáginas.
|
||||
|
||||
presupuesto.info.presupuestos-anonimos-view=Estos presupuestos son anónimos y no están asociados a ningún cliente. No se guardarán los cambios (sólo consulta).
|
||||
@ -928,7 +928,7 @@ File: Main Css File
|
||||
font-family: "Public Sans", sans-serif;
|
||||
}
|
||||
.navbar-menu .navbar-nav .nav-sm .nav-link:before {
|
||||
content: "";
|
||||
content: none;
|
||||
width: 6px;
|
||||
height: 1.5px;
|
||||
background-color: var(--vz-vertical-menu-sub-item-color);
|
||||
@ -1780,15 +1780,15 @@ File: Main Css File
|
||||
}
|
||||
[data-layout=semibox] .main-content {
|
||||
margin-left: calc(250px + 25px);
|
||||
padding: 0 6%;
|
||||
padding: 0 2%;
|
||||
}
|
||||
[data-layout=semibox] .footer {
|
||||
left: calc(250px + 6% + 1.5rem + 25px);
|
||||
right: calc(6% + 1.5rem);
|
||||
left: calc(250px + 2% + 1.5rem + 25px);
|
||||
right: calc(2% + 1.5rem);
|
||||
}
|
||||
[data-layout=semibox] #page-topbar {
|
||||
left: calc(250px + 6% + 1.5rem + 25px);
|
||||
right: calc(6% + 1.5rem);
|
||||
left: calc(250px + 2% + 1.5rem + 25px);
|
||||
right: calc(2% + 1.5rem);
|
||||
top: 25px;
|
||||
border-radius: 0.25rem;
|
||||
-webkit-transition: all 0.5s ease;
|
||||
@ -1801,10 +1801,10 @@ File: Main Css File
|
||||
margin-left: calc(180px + 25px);
|
||||
}
|
||||
[data-layout=semibox][data-sidebar-size=md] #page-topbar {
|
||||
left: calc(180px + 6% + 1.5rem + 25px);
|
||||
left: calc(180px + 2% + 1.5rem + 25px);
|
||||
}
|
||||
[data-layout=semibox][data-sidebar-size=md] .footer {
|
||||
left: calc(180px + 6% + 1.5rem + 25px);
|
||||
left: calc(180px + 2% + 1.5rem + 25px);
|
||||
}
|
||||
[data-layout=semibox][data-sidebar-size=sm] .main-content {
|
||||
margin-left: calc(70px + 25px);
|
||||
@ -1813,19 +1813,19 @@ File: Main Css File
|
||||
top: 25px;
|
||||
}
|
||||
[data-layout=semibox][data-sidebar-size=sm] #page-topbar {
|
||||
left: calc(70px + 6% + 1.5rem + 25px);
|
||||
left: calc(70px + 2% + 1.5rem + 25px);
|
||||
}
|
||||
[data-layout=semibox][data-sidebar-size=sm] .footer {
|
||||
left: calc(70px + 6% + 1.5rem + 25px);
|
||||
left: calc(70px + 2% + 1.5rem + 25px);
|
||||
}
|
||||
[data-layout=semibox][data-sidebar-size=sm-hover] .main-content {
|
||||
margin-left: calc(70px + 25px);
|
||||
}
|
||||
[data-layout=semibox][data-sidebar-size=sm-hover] #page-topbar {
|
||||
left: calc(70px + 6% + 1.5rem + 25px);
|
||||
left: calc(70px + 2% + 1.5rem + 25px);
|
||||
}
|
||||
[data-layout=semibox][data-sidebar-size=sm-hover] .footer {
|
||||
left: calc(70px + 6% + 1.5rem + 25px);
|
||||
left: calc(70px + 2% + 1.5rem + 25px);
|
||||
}
|
||||
}
|
||||
[data-layout=semibox] .mx-n4 {
|
||||
@ -6714,6 +6714,38 @@ a {
|
||||
border-left-color: #ff7f5d;
|
||||
}
|
||||
|
||||
/* ---- NAV SECUNDARIO CON PESTAÑITA OUTLINE ---- */
|
||||
/* Solo la pestaña activa: estilo outline + pestañita */
|
||||
.nav-secondary-outline.arrow-navtabs .nav-link.active {
|
||||
color: #ff7f5d;
|
||||
background: transparent;
|
||||
border: 1px solid #ff7f5d; /* como el botón Iniciar sesión */
|
||||
border-radius: .5rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Si el tema ya dibuja una flecha sólida, la anulamos */
|
||||
.nav-secondary-outline.arrow-navtabs .nav-link.active::before {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Pestañita tipo outline */
|
||||
.nav-secondary-outline.arrow-navtabs .nav-link.active::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: -7px; /* ajusta si lo necesitas */
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
transform: translateX(-50%) rotate(225deg);
|
||||
background: var(--vz-body-bg, var(--bs-body-bg, #fff)); /* color del fondo de la página */
|
||||
border-left: 1px solid #ff7f5d;
|
||||
border-top: 1px solid #ff7f5d;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.nav-success .nav-link.active {
|
||||
color: #fff;
|
||||
background-color: #3cd188;
|
||||
@ -16329,4 +16361,5 @@ span.flatpickr-weekday {
|
||||
position: absolute;
|
||||
bottom: -18px;
|
||||
left: -35px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
59
src/main/resources/static/assets/css/bootstrap-for-pdf.css
vendored
Normal file
59
src/main/resources/static/assets/css/bootstrap-for-pdf.css
vendored
Normal file
@ -0,0 +1,59 @@
|
||||
/* =======================
|
||||
Bootstrap for PDF (compatible con OpenHTMLtoPDF)
|
||||
======================= */
|
||||
|
||||
/* -- TEXT ALIGN -- */
|
||||
.text-start { text-align: left !important; }
|
||||
.text-center { text-align: center !important; }
|
||||
.text-end { text-align: right !important; }
|
||||
|
||||
/* -- FONT WEIGHT -- */
|
||||
.fw-normal { font-weight: 400 !important; }
|
||||
.fw-semibold { font-weight: 600 !important; }
|
||||
.fw-bold { font-weight: 700 !important; }
|
||||
|
||||
/* -- SPACING (margin/padding) -- */
|
||||
.mt-1 { margin-top: 0.25rem !important; }
|
||||
.mt-2 { margin-top: 0.5rem !important; }
|
||||
.mt-3 { margin-top: 1rem !important; }
|
||||
.mb-1 { margin-bottom: 0.25rem !important; }
|
||||
.mb-2 { margin-bottom: 0.5rem !important; }
|
||||
.mb-3 { margin-bottom: 1rem !important; }
|
||||
|
||||
.p-1 { padding: 0.25rem !important; }
|
||||
.p-2 { padding: 0.5rem !important; }
|
||||
.p-3 { padding: 1rem !important; }
|
||||
|
||||
/* -- TABLE -- */
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 10.5pt;
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 6px;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
background-color: #f8f9fa;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* -- BORDER -- */
|
||||
.border {
|
||||
border: 1px solid #dee2e6 !important;
|
||||
}
|
||||
|
||||
.border-0 { border: 0 !important; }
|
||||
|
||||
/* -- BACKGROUND -- */
|
||||
.bg-light { background-color: #f8f9fa !important; }
|
||||
.bg-white { background-color: #fff !important; }
|
||||
|
||||
/* -- DISPLAY UTILS (limited) -- */
|
||||
.d-block { display: block !important; }
|
||||
.d-inline { display: inline !important; }
|
||||
.d-inline-block { display: inline-block !important; }
|
||||
@ -1,3 +1,10 @@
|
||||
/* habilita container queries en tu contenedor principal */
|
||||
#presupuesto-row {
|
||||
container-type: inline-size;
|
||||
container-name: presupuesto;
|
||||
}
|
||||
|
||||
|
||||
/* === Contenedor de cada opción === */
|
||||
.image-container {
|
||||
position: relative;
|
||||
@ -23,10 +30,6 @@
|
||||
transition: border 0.3s ease;
|
||||
}
|
||||
|
||||
/* === Borde visible cuando está seleccionada === */
|
||||
.image-container.selected {
|
||||
border-color: #92b2a7;
|
||||
}
|
||||
|
||||
/* === Imagen interna === */
|
||||
.image-container img {
|
||||
@ -37,6 +40,16 @@
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
/* Borde cuando está seleccionado, sin JS */
|
||||
.image-container:has(input:checked) {
|
||||
border-color: #92b2a7;
|
||||
}
|
||||
|
||||
/* Si quieres la animación al seleccionar */
|
||||
.image-container:has(input:checked) .image-presupuesto {
|
||||
animation: zoomPop 800ms cubic-bezier(0.68,-0.55,0.27,1.55);
|
||||
}
|
||||
|
||||
/* === Animación de zoom con rebote === */
|
||||
.image-presupuesto.zoom-anim {
|
||||
animation: zoomPop 800ms cubic-bezier(0.68, -0.55, 0.27, 1.55);
|
||||
@ -242,6 +255,10 @@
|
||||
transform-origin: center center;
|
||||
will-change: transform;
|
||||
transition: transform .22s ease, box-shadow .22s ease, border-color .22s ease, background .22s ease;
|
||||
display: block;
|
||||
width: clamp(155px, 28vw, 250px); /* crece fluido pero nunca <155 ni >250 */
|
||||
min-width: 155px !important;
|
||||
max-width: 250px !important;
|
||||
}
|
||||
|
||||
/* sin elevación al hover */
|
||||
@ -408,6 +425,7 @@
|
||||
|
||||
.form-switch-presupuesto .form-check-input:checked {
|
||||
border-color: #92b2a7;
|
||||
background-color: #cbcecd;
|
||||
}
|
||||
|
||||
.form-switch-custom.form-switch-presupuesto .form-check-input:checked::before {
|
||||
@ -478,4 +496,85 @@
|
||||
#presupuesto-row .summary-col{
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* ==== BOTONES ==== */
|
||||
/* --- Base de la barra (dos filas) --- */
|
||||
.buttons-bar{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:.75rem;
|
||||
width:100%;
|
||||
}
|
||||
|
||||
/* Fila 1: acciones centradas */
|
||||
.buttons-row.center{
|
||||
display:flex;
|
||||
flex-wrap:wrap;
|
||||
justify-content:center;
|
||||
gap:.75rem;
|
||||
}
|
||||
|
||||
/* Fila 2: extremos (prev izq / next dcha) */
|
||||
.buttons-row.split{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap:.75rem;
|
||||
}
|
||||
|
||||
/* El “slot” derecho (puede contener Siguiente o Login) */
|
||||
.buttons-row.split .right-slot{
|
||||
margin-left:auto; /* en desktop empuja a la derecha */
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:.75rem;
|
||||
}
|
||||
|
||||
/* --- Limpieza de márgenes heredados para consistencia de gap --- */
|
||||
.buttons-bar .btn.mx-2{ margin-left:0 !important; margin-right:0 !important; }
|
||||
|
||||
/* ================= MOBILE / CONTENEDOR ESTRECHO ================= */
|
||||
/* A partir de aquí, todo ocurre cuando el contenedor #presupuesto-row
|
||||
es estrecho (p.ej., móvil u offcanvas abierto). Ajusta 576px si lo necesitas */
|
||||
@container presupuesto (max-width: 576px){
|
||||
|
||||
/* Fila 1: acciones apiladas y a 100% ancho */
|
||||
.buttons-row.center{
|
||||
width:100%;
|
||||
}
|
||||
.buttons-row.center .btn{
|
||||
width:100%;
|
||||
justify-content:center; /* icono + texto centrados */
|
||||
max-width: 100%; /* sin límites */
|
||||
}
|
||||
|
||||
/* Fila 2: apilar elementos; cada bloque a 100% */
|
||||
.buttons-row.split{
|
||||
flex-direction:column;
|
||||
align-items:stretch; /* hace que los hijos estiren a 100% */
|
||||
justify-content:flex-start;
|
||||
}
|
||||
.buttons-row.split > *{
|
||||
width:100%;
|
||||
}
|
||||
|
||||
/* Botón “Anterior” si existe → 100% y centrado */
|
||||
.buttons-row.split > button.btn{
|
||||
width:100%;
|
||||
justify-content:center;
|
||||
}
|
||||
|
||||
/* Bloque derecho (Siguiente / Login) a 100% */
|
||||
.buttons-row.split .right-slot{
|
||||
width:100%;
|
||||
margin-left:0; /* neutraliza el empuje a la derecha */
|
||||
justify-content:stretch; /* estira su contenido */
|
||||
}
|
||||
.buttons-row.split .right-slot .btn{
|
||||
width:100%;
|
||||
justify-content:center; /* texto centrado */
|
||||
}
|
||||
}
|
||||
395
src/main/resources/static/assets/css/presupuestopdf.css
Normal file
395
src/main/resources/static/assets/css/presupuestopdf.css
Normal file
@ -0,0 +1,395 @@
|
||||
:root {
|
||||
--verde: #92b2a7;
|
||||
--letterspace: 8px;
|
||||
/* ← puedes ajustar este valor en el root */
|
||||
-ink: #1b1e28;
|
||||
--muted: #5b6472;
|
||||
--accent: #0ea5e9;
|
||||
/* azul tira a cyan */
|
||||
--line: #e6e8ef;
|
||||
--bg-tag: #f4f7fb;
|
||||
}
|
||||
|
||||
/* Open Sans (rutas relativas desde css → fonts) */
|
||||
@font-face {
|
||||
font-family: "Open Sans";
|
||||
src: url("../fonts/OpenSans-Regular.ttf") format("truetype");
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Open Sans";
|
||||
src: url("../fonts/OpenSans-SemiBold.ttf") format("truetype");
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Open Sans";
|
||||
src: url("../fonts/OpenSans-Bold.ttf") format("truetype");
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family: "Open Sans" !important;
|
||||
color: var(--ink);
|
||||
font-size: 11pt;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
padding: 15mm 14mm 28mm 14mm; /* ↑ deja 10mm extra para no pisar el footer */
|
||||
box-sizing: border-box; /* para que el padding no desborde */
|
||||
}
|
||||
|
||||
|
||||
body.has-watermark {
|
||||
background-image: none !important;
|
||||
}
|
||||
|
||||
/* ====== HEADER (tabla) ====== */
|
||||
.il-header {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 0 0 8mm 0;
|
||||
/* ↓ espacio bajo el header */
|
||||
}
|
||||
|
||||
.il-left,
|
||||
.il-right {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.il-left {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.il-right {
|
||||
width: 50%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.il-logo {
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
/* ← tamaño logo */
|
||||
|
||||
/* Caja superior derecha con esquinas */
|
||||
.il-company-box {
|
||||
display: inline-block;
|
||||
align-items: end;
|
||||
/* para alinear a la derecha sin ocupar todo */
|
||||
position: relative;
|
||||
padding: 4mm 4mm;
|
||||
/* ← espacio texto ↔ esquinas */
|
||||
color: #000;
|
||||
font-size: 10.5pt;
|
||||
/* ← tamaño de letra */
|
||||
line-height: 1;
|
||||
/* ← separación entre líneas */
|
||||
max-width: 75mm;
|
||||
/* ← ancho máximo de la caja */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Esquinas */
|
||||
.il-company-box .corner {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
/* ← anchura esquina */
|
||||
height: 20px;
|
||||
/* ← altura esquina */
|
||||
border-color: #92b2a7;
|
||||
/* ← color esquina */
|
||||
}
|
||||
|
||||
.corner.tl {
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-top: 2px solid #92b2a7;
|
||||
border-left: 2px solid #92b2a7;
|
||||
}
|
||||
|
||||
.corner.tr {
|
||||
top: 0;
|
||||
right: 0;
|
||||
border-top: 2px solid #92b2a7;
|
||||
border-right: 2px solid #92b2a7;
|
||||
}
|
||||
|
||||
.corner.bl {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
border-bottom: 2px solid #92b2a7;
|
||||
border-left: 2px solid #92b2a7;
|
||||
}
|
||||
|
||||
.corner.br {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
border-bottom: 2px solid #92b2a7;
|
||||
border-right: 2px solid #92b2a7;
|
||||
}
|
||||
|
||||
|
||||
.company-line {
|
||||
margin: 1.5mm 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Nueva banda verde PRESUPUESTO */
|
||||
.doc-banner {
|
||||
width: 100%;
|
||||
background-color: #92b2a7 !important; /* ← tu verde corporativo */
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 2mm 0;
|
||||
margin-bottom: 4mm;
|
||||
display: block; /* evita conflictos */
|
||||
}
|
||||
|
||||
.banner-text {
|
||||
font-family: "Open Sans", Arial, sans-serif !important;
|
||||
font-weight: 400;
|
||||
font-size: 20pt;
|
||||
letter-spacing: 8px; /* ← configurable */
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* ficha superior */
|
||||
.sheet-info {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 4mm 0 6mm 0;
|
||||
font-size: 10.5pt;
|
||||
}
|
||||
|
||||
.sheet-info td {
|
||||
border: 1px solid var(--line);
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
.sheet-info .lbl {
|
||||
color: var(--muted);
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
/*.sheet-info .val {
|
||||
}*/
|
||||
|
||||
/* Línea título libro */
|
||||
.line-title {
|
||||
font-family: "Open Sans", Arial, sans-serif !important;
|
||||
margin: 3mm 0 5mm 0;
|
||||
padding: 2px 4px;
|
||||
font-size: 10.5pt;
|
||||
font-weight: 600;
|
||||
color: #5c5c5c;
|
||||
}
|
||||
|
||||
.line-title .lbl {
|
||||
margin-right: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Specs 2 columnas */
|
||||
.specs-wrapper {
|
||||
width: 180mm;
|
||||
margin-left: 15mm; /* ← margen izquierdo real del A4 */
|
||||
margin-right: auto; /* opcional */
|
||||
color: #5c5c5c;
|
||||
font-size: 9pt;
|
||||
}
|
||||
|
||||
.align-with-text {
|
||||
margin-left: 1mm;
|
||||
margin-right: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.specs {
|
||||
display: table;
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
margin-bottom: 6mm;
|
||||
}
|
||||
.specs .col {
|
||||
display: table-cell;
|
||||
width: 50%;
|
||||
padding-right: 6mm;
|
||||
vertical-align: top;
|
||||
}
|
||||
.specs .col:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
/* Listas sin margen superior por defecto */
|
||||
ul, ol {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0rem; /* si quieres algo abajo */
|
||||
padding-left: 1.25rem; /* sangría */
|
||||
}
|
||||
|
||||
/* Párrafos con menos margen inferior */
|
||||
p {
|
||||
margin: 0 0 .5rem;
|
||||
}
|
||||
|
||||
/* Si una lista va justo después de un texto o título, que no tenga hueco arriba */
|
||||
p + ul, p + ol,
|
||||
h1 + ul, h2 + ul, h3 + ul, h4 + ul, h5 + ul, h6 + ul,
|
||||
div + ul, div + ol {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
|
||||
.block-title {
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
font-size: 8pt;
|
||||
margin: 2mm 0 1mm 0;
|
||||
}
|
||||
|
||||
.kv {
|
||||
margin: 1mm 0;
|
||||
}
|
||||
|
||||
.kv span {
|
||||
color: var(--muted);
|
||||
display: inline-block;
|
||||
min-width: 55%;
|
||||
}
|
||||
|
||||
.kv b {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.subblock {
|
||||
margin-top: 3mm;
|
||||
}
|
||||
|
||||
.services {
|
||||
margin: 0;
|
||||
padding-left: 14px;
|
||||
}
|
||||
|
||||
.services li {
|
||||
margin: 1mm 0;
|
||||
}
|
||||
|
||||
/* Bloque marcapáginas */
|
||||
.bookmark {
|
||||
margin-top: 4mm;
|
||||
border: 1px dashed var(--line);
|
||||
padding: 3mm;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.bookmark .bk-title {
|
||||
font-weight: 700;
|
||||
margin-bottom: 2mm;
|
||||
}
|
||||
|
||||
/* Tabla de precios (tiradas) */
|
||||
.prices {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 6mm;
|
||||
font-size: 10.5pt;
|
||||
}
|
||||
|
||||
.prices thead th {
|
||||
text-align: left;
|
||||
padding: 3px;
|
||||
border-bottom: 2px solid var(--accent);
|
||||
background: #eef8fe;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.prices tbody td {
|
||||
border-bottom: 1px solid var(--line);
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.prices .col-tirada {
|
||||
width: 22%;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
|
||||
.footer {
|
||||
position: fixed;
|
||||
left: 14mm;
|
||||
right: 14mm;
|
||||
bottom: 18mm;
|
||||
border-top: 1px solid var(--line);
|
||||
padding-top: 4mm;
|
||||
font-size: 7.5pt;
|
||||
color: var(--muted);
|
||||
z-index: 10; /* sobre la marca */
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
|
||||
.footer .address {
|
||||
display: table-cell;
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
.footer .privacy {
|
||||
display: table-cell;
|
||||
width: 55%;
|
||||
}
|
||||
|
||||
.pv-title {
|
||||
font-weight: 700;
|
||||
margin-bottom: 1mm;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.pv-text {
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.page-count {
|
||||
margin-top: 2mm;
|
||||
text-align: right;
|
||||
font-size: 9pt;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.page::after {
|
||||
content: counter(page);
|
||||
}
|
||||
|
||||
.pages::after {
|
||||
content: counter(pages);
|
||||
}
|
||||
|
||||
/* Caja a página completa SIN vw/vh y SIN z-index negativo */
|
||||
.watermark {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0; /* ocupa toda la HOJA */
|
||||
pointer-events: none;
|
||||
z-index: 0; /* debajo del contenido */
|
||||
}
|
||||
|
||||
.watermark img {
|
||||
position: absolute;
|
||||
top: 245mm; /* baja/sube (70–85%) */
|
||||
left: 155mm; /* desplaza a la derecha si quieres */
|
||||
transform: translate(-50%, -50%) rotate(-15deg);
|
||||
width: 60%; /* tamaño grande, ya no hay recorte por márgenes */
|
||||
max-width: none;
|
||||
}
|
||||
BIN
src/main/resources/static/assets/fonts/OpenSans-Bold.ttf
Normal file
BIN
src/main/resources/static/assets/fonts/OpenSans-Bold.ttf
Normal file
Binary file not shown.
BIN
src/main/resources/static/assets/fonts/OpenSans-BoldItalic.ttf
Normal file
BIN
src/main/resources/static/assets/fonts/OpenSans-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
src/main/resources/static/assets/fonts/OpenSans-ExtraBold.ttf
Normal file
BIN
src/main/resources/static/assets/fonts/OpenSans-ExtraBold.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
src/main/resources/static/assets/fonts/OpenSans-Italic.ttf
Normal file
BIN
src/main/resources/static/assets/fonts/OpenSans-Italic.ttf
Normal file
Binary file not shown.
BIN
src/main/resources/static/assets/fonts/OpenSans-Light.ttf
Normal file
BIN
src/main/resources/static/assets/fonts/OpenSans-Light.ttf
Normal file
Binary file not shown.
BIN
src/main/resources/static/assets/fonts/OpenSans-LightItalic.ttf
Normal file
BIN
src/main/resources/static/assets/fonts/OpenSans-LightItalic.ttf
Normal file
Binary file not shown.
BIN
src/main/resources/static/assets/fonts/OpenSans-Regular.ttf
Normal file
BIN
src/main/resources/static/assets/fonts/OpenSans-Regular.ttf
Normal file
Binary file not shown.
BIN
src/main/resources/static/assets/fonts/OpenSans-Semibold.ttf
Normal file
BIN
src/main/resources/static/assets/fonts/OpenSans-Semibold.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
src/main/resources/static/assets/images/logo-watermark.png
Normal file
BIN
src/main/resources/static/assets/images/logo-watermark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
@ -22,7 +22,7 @@
|
||||
if (!lang || lang === getCurrentLang()) return;
|
||||
|
||||
// Guarda la preferencia (opcional)
|
||||
try { localStorage.setItem("language", lang); } catch {}
|
||||
try { localStorage.setItem("language", lang); } catch { }
|
||||
|
||||
// Redirige con ?lang=... para que Spring cambie el Locale y renderice en ese idioma
|
||||
const url = new URL(location.href);
|
||||
@ -48,7 +48,212 @@
|
||||
url.searchParams.set("lang", saved);
|
||||
location.replace(url); // alinea y no deja historial extra
|
||||
}
|
||||
|
||||
if (document.getElementById("topnav-hamburger-icon")) {
|
||||
document.getElementById("topnav-hamburger-icon").addEventListener("click", toggleHamburgerMenu);
|
||||
}
|
||||
|
||||
window.addEventListener("resize", windowResizeHover);
|
||||
windowResizeHover();
|
||||
|
||||
initsAlert();
|
||||
}
|
||||
|
||||
function windowResizeHover() {
|
||||
feather.replace();
|
||||
var windowSize = document.documentElement.clientWidth;
|
||||
if (windowSize < 1025 && windowSize > 767) {
|
||||
document.body.classList.remove("twocolumn-panel");
|
||||
if (sessionStorage.getItem("data-layout") == "twocolumn") {
|
||||
document.documentElement.setAttribute("data-layout", "twocolumn");
|
||||
if (document.getElementById("customizer-layout03")) {
|
||||
document.getElementById("customizer-layout03").click();
|
||||
}
|
||||
twoColumnMenuGenerate();
|
||||
initTwoColumnActiveMenu();
|
||||
isCollapseMenu();
|
||||
}
|
||||
if (sessionStorage.getItem("data-layout") == "vertical") {
|
||||
document.documentElement.setAttribute("data-sidebar-size", "sm");
|
||||
}
|
||||
if (document.querySelector(".hamburger-icon")) {
|
||||
document.querySelector(".hamburger-icon").classList.add("open");
|
||||
}
|
||||
} else if (windowSize >= 1025) {
|
||||
document.body.classList.remove("twocolumn-panel");
|
||||
if (sessionStorage.getItem("data-layout") == "twocolumn") {
|
||||
document.documentElement.setAttribute("data-layout", "twocolumn");
|
||||
if (document.getElementById("customizer-layout03")) {
|
||||
document.getElementById("customizer-layout03").click();
|
||||
}
|
||||
twoColumnMenuGenerate();
|
||||
initTwoColumnActiveMenu();
|
||||
isCollapseMenu();
|
||||
}
|
||||
if (sessionStorage.getItem("data-layout") == "vertical") {
|
||||
document.documentElement.setAttribute(
|
||||
"data-sidebar-size",
|
||||
sessionStorage.getItem("data-sidebar-size")
|
||||
);
|
||||
}
|
||||
if (document.querySelector(".hamburger-icon")) {
|
||||
document.querySelector(".hamburger-icon").classList.remove("open");
|
||||
}
|
||||
} else if (windowSize <= 767) {
|
||||
document.body.classList.remove("vertical-sidebar-enable");
|
||||
document.body.classList.add("twocolumn-panel");
|
||||
if (sessionStorage.getItem("data-layout") == "twocolumn") {
|
||||
document.documentElement.setAttribute("data-layout", "vertical");
|
||||
hideShowLayoutOptions("vertical");
|
||||
isCollapseMenu();
|
||||
}
|
||||
if (sessionStorage.getItem("data-layout") != "horizontal") {
|
||||
document.documentElement.setAttribute("data-sidebar-size", "lg");
|
||||
}
|
||||
if (document.querySelector(".hamburger-icon")) {
|
||||
document.querySelector(".hamburger-icon").classList.add("open");
|
||||
}
|
||||
}
|
||||
|
||||
var isElement = document.querySelectorAll("#navbar-nav > li.nav-item");
|
||||
Array.from(isElement).forEach(function (item) {
|
||||
item.addEventListener("click", menuItem.bind(this), false);
|
||||
item.addEventListener("mouseover", menuItem.bind(this), false);
|
||||
});
|
||||
}
|
||||
|
||||
function menuItem(e) {
|
||||
if (e.target && e.target.matches("a.nav-link span")) {
|
||||
if (elementInViewport(e.target.parentElement.nextElementSibling) == false) {
|
||||
e.target.parentElement.nextElementSibling.classList.add("dropdown-custom-right");
|
||||
e.target.parentElement.parentElement.parentElement.parentElement.classList.add("dropdown-custom-right");
|
||||
var eleChild = e.target.parentElement.nextElementSibling;
|
||||
Array.from(eleChild.querySelectorAll(".menu-dropdown")).forEach(function (item) {
|
||||
item.classList.add("dropdown-custom-right");
|
||||
});
|
||||
} else if (elementInViewport(e.target.parentElement.nextElementSibling) == true) {
|
||||
if (window.innerWidth >= 1848) {
|
||||
var elements = document.getElementsByClassName("dropdown-custom-right");
|
||||
while (elements.length > 0) {
|
||||
elements[0].classList.remove("dropdown-custom-right");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (e.target && e.target.matches("a.nav-link")) {
|
||||
if (elementInViewport(e.target.nextElementSibling) == false) {
|
||||
e.target.nextElementSibling.classList.add("dropdown-custom-right");
|
||||
e.target.parentElement.parentElement.parentElement.classList.add("dropdown-custom-right");
|
||||
var eleChild = e.target.nextElementSibling;
|
||||
Array.from(eleChild.querySelectorAll(".menu-dropdown")).forEach(function (item) {
|
||||
item.classList.add("dropdown-custom-right");
|
||||
});
|
||||
} else if (elementInViewport(e.target.nextElementSibling) == true) {
|
||||
if (window.innerWidth >= 1848) {
|
||||
var elements = document.getElementsByClassName("dropdown-custom-right");
|
||||
while (elements.length > 0) {
|
||||
elements[0].classList.remove("dropdown-custom-right");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function elementInViewport(el) {
|
||||
if (el) {
|
||||
var top = el.offsetTop;
|
||||
var left = el.offsetLeft;
|
||||
var width = el.offsetWidth;
|
||||
var height = el.offsetHeight;
|
||||
|
||||
if (el.offsetParent) {
|
||||
while (el.offsetParent) {
|
||||
el = el.offsetParent;
|
||||
top += el.offsetTop;
|
||||
left += el.offsetLeft;
|
||||
}
|
||||
}
|
||||
return (
|
||||
top >= window.pageYOffset &&
|
||||
left >= window.pageXOffset &&
|
||||
top + height <= window.pageYOffset + window.innerHeight &&
|
||||
left + width <= window.pageXOffset + window.innerWidth
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function initsAlert() {
|
||||
var alerts = document.querySelectorAll('.alert.alert-dismissible');
|
||||
alerts.forEach(function (el) {
|
||||
// Solo si está visible
|
||||
if (el.classList.contains('show')) {
|
||||
setTimeout(function () {
|
||||
// Usa la API de Bootstrap para cerrar con transición
|
||||
var bsAlert = bootstrap.Alert.getOrCreateInstance(el);
|
||||
bsAlert.close();
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", initLanguage);
|
||||
|
||||
|
||||
function toggleHamburgerMenu() {
|
||||
var windowSize = document.documentElement.clientWidth;
|
||||
|
||||
if (windowSize > 767)
|
||||
document.querySelector(".hamburger-icon").classList.toggle("open");
|
||||
|
||||
//For collapse horizontal menu
|
||||
if (document.documentElement.getAttribute("data-layout") === "horizontal") {
|
||||
document.body.classList.contains("menu") ? document.body.classList.remove("menu") : document.body.classList.add("menu");
|
||||
}
|
||||
|
||||
//For collapse vertical menu
|
||||
if (document.documentElement.getAttribute("data-layout") === "vertical") {
|
||||
if (windowSize <= 1025 && windowSize > 767) {
|
||||
document.body.classList.remove("vertical-sidebar-enable");
|
||||
document.documentElement.getAttribute("data-sidebar-size") == "sm" ?
|
||||
document.documentElement.setAttribute("data-sidebar-size", "") :
|
||||
document.documentElement.setAttribute("data-sidebar-size", "sm");
|
||||
} else if (windowSize > 1025) {
|
||||
document.body.classList.remove("vertical-sidebar-enable");
|
||||
document.documentElement.getAttribute("data-sidebar-size") == "lg" ?
|
||||
document.documentElement.setAttribute("data-sidebar-size", "sm") :
|
||||
document.documentElement.setAttribute("data-sidebar-size", "lg");
|
||||
} else if (windowSize <= 767) {
|
||||
document.body.classList.add("vertical-sidebar-enable");
|
||||
document.documentElement.setAttribute("data-sidebar-size", "lg");
|
||||
}
|
||||
}
|
||||
|
||||
// semibox menu
|
||||
if (document.documentElement.getAttribute("data-layout") === "semibox") {
|
||||
if (windowSize > 767) {
|
||||
// (document.querySelector(".hamburger-icon").classList.contains("open")) ? document.documentElement.setAttribute('data-sidebar-visibility', "show"): '';
|
||||
if (document.documentElement.getAttribute('data-sidebar-visibility') == "show") {
|
||||
document.documentElement.getAttribute("data-sidebar-size") == "lg" ?
|
||||
document.documentElement.setAttribute("data-sidebar-size", "sm") :
|
||||
document.documentElement.setAttribute("data-sidebar-size", "lg");
|
||||
} else {
|
||||
document.getElementById("sidebar-visibility-show").click();
|
||||
document.documentElement.setAttribute("data-sidebar-size", document.documentElement.getAttribute("data-sidebar-size"));
|
||||
}
|
||||
} else if (windowSize <= 767) {
|
||||
document.body.classList.add("vertical-sidebar-enable");
|
||||
document.documentElement.setAttribute("data-sidebar-size", "lg");
|
||||
}
|
||||
}
|
||||
|
||||
//Two column menu
|
||||
if (document.documentElement.getAttribute("data-layout") == "twocolumn") {
|
||||
document.body.classList.contains("twocolumn-panel") ?
|
||||
document.body.classList.remove("twocolumn-panel") :
|
||||
document.body.classList.add("twocolumn-panel");
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
@ -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);
|
||||
});
|
||||
@ -0,0 +1,67 @@
|
||||
import { formateaMoneda } from '../../imprimelibros/utils.js';
|
||||
|
||||
$(() => {
|
||||
|
||||
updateTotal();
|
||||
|
||||
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;
|
||||
}
|
||||
$("#base-cesta").text(formateaMoneda(base));
|
||||
if (iva4 > 0) {
|
||||
$("#iva-4-cesta").text(formateaMoneda(iva4));
|
||||
$("#tr-iva-4").show();
|
||||
} else {
|
||||
$("#tr-iva-4").hide();
|
||||
}
|
||||
if (iva21 > 0) {
|
||||
$("#iva-21-cesta").text(formateaMoneda(iva21));
|
||||
$("#tr-iva-21").show();
|
||||
} else {
|
||||
$("#tr-iva-21").hide();
|
||||
}
|
||||
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 card = $(this).closest('.card.product');
|
||||
|
||||
// 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/delete/item/${cartItemId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { [csrfHeader]: csrfToken }
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
console.error('Error al eliminar. Status:', res.status);
|
||||
return;
|
||||
}
|
||||
else{
|
||||
card?.remove();
|
||||
updateTotal();
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error en la solicitud:', err);
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -26,6 +26,22 @@
|
||||
pageLength: 50,
|
||||
language: { url: '/assets/libs/datatables/i18n/' + language + '.json' },
|
||||
responsive: true,
|
||||
dom: 'lrBtip',
|
||||
buttons: {
|
||||
dom: {
|
||||
button: {
|
||||
className: 'btn btn-sm btn-outline-primary me-1'
|
||||
},
|
||||
buttons: [
|
||||
{ extend: 'copy' },
|
||||
{ extend: 'csv' },
|
||||
{ extend: 'excel' },
|
||||
{ extend: 'pdf' },
|
||||
{ extend: 'print' },
|
||||
{ extend: 'colvis' }
|
||||
],
|
||||
}
|
||||
},
|
||||
ajax: {
|
||||
url: '/configuracion/margenes-presupuesto/datatable',
|
||||
method: 'GET',
|
||||
|
||||
@ -7,17 +7,17 @@ class imagen_presupuesto {
|
||||
this.group = data.group || ""; // Grupo al que pertenece el radio
|
||||
this.texto = data.texto || ""; // Texto de la etiqueta
|
||||
this.selected = data.selected || false;
|
||||
this.extraClass = (data.extraClass && !(data.extraClass===undefined))? (data.extraClass + ' ') : '';
|
||||
this.extraClass = (data.extraClass && !(data.extraClass === undefined)) ? (data.extraClass + ' ') : '';
|
||||
this.extraData = data.extra_data || {}; // Datos extra opcionales
|
||||
}
|
||||
|
||||
render() {
|
||||
const contenedor = $('<div>', {
|
||||
const contenedor = $('<label>', {
|
||||
id: this.id,
|
||||
class: `${this.extraClass + ' '}image-container imagen-selector${this.selected ? ' selected' : ''}`
|
||||
class: `${this.extraClass + ' '}image-container imagen-selector}`
|
||||
});
|
||||
// Añadir atributos extra al contenedor
|
||||
for (const [key, value] of Object.entries(this.extraData)) {
|
||||
for (const [key, value] of Object.entries(this.extraData)) {
|
||||
contenedor.attr(`data-${key}`, value);
|
||||
}
|
||||
contenedor.attr("data-summary-text", this.texto); // Para el resumen
|
||||
@ -26,7 +26,8 @@ class imagen_presupuesto {
|
||||
type: 'radio',
|
||||
name: this.group,
|
||||
value: this.id,
|
||||
hidden: true
|
||||
hidden: true,
|
||||
checked: this.selected
|
||||
});
|
||||
|
||||
|
||||
@ -37,13 +38,12 @@ class imagen_presupuesto {
|
||||
alt: this.alt
|
||||
});
|
||||
|
||||
const etiqueta = $('<label>', {
|
||||
for: this.id + '-img',
|
||||
class: 'form-label',
|
||||
const etiqueta = $('<div>', {
|
||||
class: 'form-text text-center',
|
||||
text: this.texto
|
||||
});
|
||||
|
||||
contenedor.append(imagen, etiqueta);
|
||||
contenedor.append(input, imagen, etiqueta);
|
||||
return contenedor;
|
||||
}
|
||||
|
||||
|
||||
@ -1,20 +1,24 @@
|
||||
$('.imagen-container-group').on('click', '.image-container', function (e) {
|
||||
e.preventDefault(); // <- evita que el label dispare el cambio nativo (2º change)
|
||||
e.stopPropagation();
|
||||
|
||||
$('.imagen-container-group').on('click', '.image-container', function () {
|
||||
const clicked = $(this);
|
||||
const group = clicked.closest('.imagen-container-group');
|
||||
|
||||
// Limpiar selección anterior
|
||||
// Si ya está seleccionado, no hagas nada
|
||||
const $radio = clicked.find('input[type="radio"]');
|
||||
if ($radio.prop('checked')) return;
|
||||
|
||||
// Limpiar selección anterior (solo clases/animación)
|
||||
group.find('.image-container').removeClass('selected')
|
||||
.find('.image-presupuesto').removeClass('zoom-anim');
|
||||
|
||||
// Marcar nuevo seleccionado
|
||||
// Marcar nuevo seleccionado (clases/animación)
|
||||
clicked.addClass('selected');
|
||||
|
||||
// Aplicar animación de zoom
|
||||
const img = clicked.find('.image-presupuesto');
|
||||
void img[0].offsetWidth; // Forzar reflow
|
||||
void img[0].offsetWidth;
|
||||
img.addClass('zoom-anim');
|
||||
|
||||
|
||||
clicked.find('input[type="radio"]').prop('checked', true).trigger('change');
|
||||
// Marca el radio y dispara UN único change
|
||||
$radio.prop('checked', true).trigger('change');
|
||||
});
|
||||
|
||||
@ -3,7 +3,6 @@ import { formateaMoneda } from "../utils.js";
|
||||
|
||||
|
||||
$(document).on('change', '#maquetacion', function (e) {
|
||||
|
||||
e.preventDefault();
|
||||
if ($('#maquetacion').is(':checked')) {
|
||||
$.get("/presupuesto/public/maquetacion/form", function (data) {
|
||||
|
||||
@ -152,11 +152,11 @@ $(document).on('change', '.marcapaginas-item', () => {
|
||||
|
||||
const payload = {
|
||||
marcapaginas_tirada: parseInt($('#marcapaginas-tirada').val()) || 100,
|
||||
tamanio_marcapaginas: $('#tamanio-marcapaginas').val() || '_50x140_',
|
||||
caras_impresion: $('#caras-impresion').val() || 'una_cara',
|
||||
papel_marcapaginas: $('#papel-marcapaginas').val() || 'cartulina_grafica',
|
||||
gramaje_marcapaginas: parseInt($('#gramaje-marcapaginas').val()) || 300,
|
||||
acabado_marcapaginas: $('#acabado-marcapaginas').val() || 'ninguno',
|
||||
tamanio: $('#tamanio-marcapaginas').val() || '_50x140_',
|
||||
carasImpresion: $('#caras-impresion').val() || 'una_cara',
|
||||
papel: $('#papel-marcapaginas').val() || 'cartulina_grafica',
|
||||
gramaje: parseInt($('#gramaje-marcapaginas').val()) || 300,
|
||||
acabado: $('#acabado-marcapaginas').val() || 'ninguno',
|
||||
};
|
||||
$(document).trigger('marcapaginas:update', [payload]);
|
||||
});
|
||||
@ -167,11 +167,11 @@ function loadMarcapaginasData() {
|
||||
|
||||
$(document).one('marcapaginas:response', (e, stored) => {
|
||||
$('#marcapaginas-tirada').val(stored.marcapaginas_tirada);
|
||||
$('#tamanio-marcapaginas').val(stored.tamanio_marcapaginas);
|
||||
$('#caras-impresion').val(stored.caras_impresion);
|
||||
$('#papel-marcapaginas').val(stored.papel_marcapaginas);
|
||||
$('#gramaje-marcapaginas').val(stored.gramaje_marcapaginas);
|
||||
$('#acabado-marcapaginas').val(stored.acabado_marcapaginas);
|
||||
$('#tamanio-marcapaginas').val(stored.tamanio);
|
||||
$('#caras-impresion').val(stored.carasImpresion);
|
||||
$('#papel-marcapaginas').val(stored.papel);
|
||||
$('#gramaje-marcapaginas').val(stored.gramaje);
|
||||
$('#acabado-marcapaginas').val(stored.acabado);
|
||||
});
|
||||
|
||||
$(document).trigger('marcapaginas:request');
|
||||
|
||||
@ -7,9 +7,9 @@ export function updateEncuadernacion() {
|
||||
}
|
||||
}
|
||||
|
||||
export function updateFormato(){
|
||||
export function updateFormato() {
|
||||
|
||||
if($('#formato-personalizado').is(':checked')) {
|
||||
if ($('#formato-personalizado').is(':checked')) {
|
||||
$('#summary-formato').text($('#ancho').val() + 'x' + $('#alto').val() + ' mm');
|
||||
} else {
|
||||
const $selected = $('#formato option:selected');
|
||||
@ -21,29 +21,29 @@ export function updateFormato(){
|
||||
export function updatePaginas() {
|
||||
|
||||
const paginas = $('#paginas').val();
|
||||
$('#summary-paginas').text(paginas );
|
||||
$('#summary-paginas').text(paginas);
|
||||
|
||||
const paginasColor = $('#paginas-color').val();
|
||||
$('#summary-paginas-color').text(paginasColor );
|
||||
$('#summary-paginas-color').text(paginasColor);
|
||||
|
||||
const paginasNegro = $('#paginas-negro').val();
|
||||
$('#summary-paginas-negro').text(paginasNegro );
|
||||
$('#summary-paginas-negro').text(paginasNegro);
|
||||
}
|
||||
|
||||
export function updateTipoImpresion() {
|
||||
|
||||
const $selected = $('.opcion-color.selected');
|
||||
const $selected = $('.opcion-color input:checked');
|
||||
if ($selected.length > 0) {
|
||||
const resumen = $selected.data('summary-text') || $selected.attr('id');
|
||||
const resumen = $selected.closest('.opcion-color').data('summary-text') || $selected.closest('.opcion-color').attr('id');
|
||||
$('#summary-tipo-interior').text(resumen);
|
||||
}
|
||||
}
|
||||
|
||||
export function updatePapelInterior() {
|
||||
|
||||
const $selected = $('.papel-interior.selected');
|
||||
const $selected = $('.papel-interior input:checked');
|
||||
if ($selected.length > 0) {
|
||||
const resumen = $selected.data('summary-text') || $selected.val();
|
||||
const resumen = $selected.closest('.papel-interior').data('summary-text') || $selected.closest('.papel-interior').val();
|
||||
$('#summary-papel-interior').text(resumen);
|
||||
}
|
||||
}
|
||||
@ -51,34 +51,36 @@ export function updatePapelInterior() {
|
||||
export function updateGramajeInterior() {
|
||||
|
||||
const gramaje = $('input[name="gramaje-interior"]:checked');
|
||||
if(gramaje.length > 0) {
|
||||
if (gramaje.length > 0) {
|
||||
$('#summary-gramaje-interior').text(gramaje.data('gramaje'));
|
||||
}
|
||||
}
|
||||
|
||||
export function updateTapaCubierta(){
|
||||
export function updateTapaCubierta() {
|
||||
|
||||
const $selected = $('.tapa-cubierta.selected');
|
||||
const $selected = $('.tapa-cubierta input:checked');
|
||||
if ($selected.length > 0) {
|
||||
const resumen = $selected.data('summary-text') || $selected.attr('id');
|
||||
const resumen = $selected.closest('.tapa-cubierta').data('summary-text') || $selected.closest('.tapa-cubierta').attr('id');
|
||||
$('#summary-tapa-cubierta').text(resumen);
|
||||
}
|
||||
if($selected.attr('id') === 'tapaBlanda') {
|
||||
if ($selected.closest('.tapa-cubierta').attr('id') === 'tapaBlanda') {
|
||||
|
||||
$('.tapa-blanda-row').removeClass('d-none');
|
||||
$('.tapa-dura-row').addClass('d-none');
|
||||
$('#summary-cubierta-solapas').text($('#sin-solapas').hasClass('selected') ? $('#sin-solapas').data('summary-text') : $('#con-solapas').data('summary-text'));
|
||||
if($('#con-solapas').hasClass('selected')) {
|
||||
$('#summary-tamanio-solapa-row').removeClass('d-none');
|
||||
const $solapasSelected = $('.solapas-cubierta input:checked');
|
||||
if ($solapasSelected.val() === 'conSolapas') {
|
||||
$('#summary-cubierta-solapas').text($('#con-solapas').data('summary-text'));
|
||||
$('.summary-tamanio-solapa-row').removeClass('d-none');
|
||||
$('#summary-tamanio-solapa').text($('#tamanio-solapas-cubierta').val() + ' mm');
|
||||
} else {
|
||||
$('#summary-tamanio-solapa-row').addClass('d-none');
|
||||
$('#summary-cubierta-solapas').text($('#sin-solapas').data('summary-text'));
|
||||
$('.summary-tamanio-solapa-row').addClass('d-none');
|
||||
$('#summary-tamanio-solapa').text('');
|
||||
}
|
||||
$('#summary-impresion-cubierta-row').removeClass('d-none');
|
||||
$('#summary-impresion-cubierta').text($('#impresion-cubierta option:selected').text());
|
||||
}
|
||||
else{
|
||||
else {
|
||||
$('.tapa-blanda-row').addClass('d-none');
|
||||
$('.tapa-dura-row').removeClass('d-none');
|
||||
$('#summary-papel-guardas').text($('#papel-guardas option:selected').text());
|
||||
@ -88,26 +90,31 @@ export function updateTapaCubierta(){
|
||||
}
|
||||
|
||||
export function updatePapelCubierta() {
|
||||
const $selected = $('.papel-cubierta.selected');
|
||||
const $selected = $('.papel-cubierta input:checked');
|
||||
if ($selected.length > 0) {
|
||||
const resumen = $selected.data('summary-text') || $selected.val();
|
||||
const resumen = $selected.closest('.papel-cubierta').data('summary-text') || $selected.closest('.papel-cubierta').val();
|
||||
$('#summary-papel-cubierta').text(resumen);
|
||||
}
|
||||
}
|
||||
|
||||
export function updateGramajeCubierta() {
|
||||
const gramaje = $('input[name="gramaje-cubierta"]:checked');
|
||||
if(gramaje.length > 0) {
|
||||
$('#summary-gramaje-cubierta').text(gramaje.data('gramaje'));
|
||||
export function updateGramajeCubierta(gramaje) {
|
||||
|
||||
if (!gramaje) {
|
||||
const gramaje = $('input[name="gramaje-cubierta"]:checked');
|
||||
if (gramaje.length > 0) {
|
||||
$('#summary-gramaje-cubierta').text(gramaje.data('gramaje'));
|
||||
}
|
||||
} else {
|
||||
$('#summary-gramaje-cubierta').text(gramaje);
|
||||
}
|
||||
}
|
||||
|
||||
export function updateAcabadoCubierta() {
|
||||
const acabado = $('input[name="acabado-cubierta"]:checked');
|
||||
if(acabado.length > 0) {
|
||||
if (acabado.length > 0) {
|
||||
let labelText = '';
|
||||
const id = acabado.attr('id');
|
||||
|
||||
|
||||
if (id) {
|
||||
labelText = $(`label[for="${id}"]`).text().trim();
|
||||
}
|
||||
@ -123,20 +130,20 @@ export function updateAcabadoCubierta() {
|
||||
|
||||
export function updateSobreCubierta() {
|
||||
|
||||
if($('#sobrecubierta').hasClass('active')) {
|
||||
if ($('#sobrecubierta').hasClass('active')) {
|
||||
$('#summary-sobrecubierta-papel-gramaje').text($('#papel-sobrecubierta option:selected').text());
|
||||
$('#summary-sobrecubierta-tamanio-solapa').text($('#tamanio-solapas-sobrecubierta').val() + ' mm');
|
||||
$('#summary-sobrecubierta-acabado').text($('#sobrecubierta-acabado option:selected').text());
|
||||
$('#summary-sobrecubierta-acabado').text($('#sobrecubierta-acabado option:selected').text());
|
||||
}
|
||||
}
|
||||
|
||||
export function updateFaja() {
|
||||
|
||||
if($('#faja').hasClass('active')) {
|
||||
if ($('#faja').hasClass('active')) {
|
||||
$('#summary-faja-papel-gramaje').text($('#papel-faja option:selected').text());
|
||||
$('#summary-faja-alto-faja').text($('#alto-faja').val() + ' mm');
|
||||
$('#summary-faja-tamanio-solapa').text($('#tamanio-solapas-faja').val() + ' mm');
|
||||
$('#summary-faja-acabado').text($('#faja-acabado option:selected').text());
|
||||
$('#summary-faja-acabado').text($('#faja-acabado option:selected').text());
|
||||
}
|
||||
}
|
||||
|
||||
@ -146,7 +153,7 @@ export function updateExtras() {
|
||||
$tbody.empty();
|
||||
|
||||
// Agregar las filas de servicios extras
|
||||
$('.service-checkbox:checked').each(function() {
|
||||
$('.service-checkbox:checked').each(function () {
|
||||
const $servicio = $(this);
|
||||
const resumen = $(`label[for="${$servicio.attr('id')}"] .service-title`).text().trim() || $servicio.attr('id');
|
||||
const price = $(`label[for="${$servicio.attr('id')}"] .service-price`).text().trim() || $servicio.attr('price');
|
||||
|
||||
@ -36,7 +36,7 @@ class TiradaCard {
|
||||
|
||||
const col = $(`
|
||||
<div class="col d-flex">
|
||||
<label class="tirada-card ${this.selected ? 'selected' : ''} w-100 h-100" for="tirada-${this.id}">
|
||||
<label class="tirada-card ${this.selected ? 'selected' : ''} h-100" for="tirada-${this.id}">
|
||||
<input type="radio" name="${this.name}" id="tirada-${this.id}" value="${this.unidades}" ${this.selected ? 'checked' : ''}>
|
||||
<div class="title">${this.#title()}</div>
|
||||
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
import PresupuestoWizard from './wizard.js';
|
||||
|
||||
const app = new PresupuestoWizard({ mode: 'public' });
|
||||
app.init();
|
||||
@ -0,0 +1,14 @@
|
||||
import PresupuestoWizard from './wizard.js';
|
||||
|
||||
if($('#presupuesto_id').val() == null || $('#presupuesto_id').val() === '') {
|
||||
sessionStorage.removeItem('formData');
|
||||
}
|
||||
|
||||
const app = new PresupuestoWizard({
|
||||
mode: 'private',
|
||||
readonly: false,
|
||||
presupuestoId: $('#presupuesto_id').val(),
|
||||
canSave: true,
|
||||
useSessionCache: true,
|
||||
});
|
||||
app.init();
|
||||
@ -0,0 +1,12 @@
|
||||
import PresupuestoWizard from './wizard.js';
|
||||
|
||||
// remove formData from sessionStorage to avoid conflicts
|
||||
sessionStorage.removeItem('formData');
|
||||
|
||||
const app = new PresupuestoWizard({
|
||||
mode: 'public',
|
||||
readonly: false,
|
||||
canSave: true,
|
||||
useSessionCache: false,
|
||||
});
|
||||
app.init();
|
||||
@ -0,0 +1,9 @@
|
||||
import PresupuestoWizard from './wizard.js';
|
||||
|
||||
const app = new PresupuestoWizard({
|
||||
mode: 'public',
|
||||
readonly: true,
|
||||
canSave: false,
|
||||
useSessionCache: true,
|
||||
});
|
||||
app.init();
|
||||
@ -4,9 +4,16 @@ import TiradaCard from "./tirada-price-card.js";
|
||||
import * as Summary from "./summary.js";
|
||||
import { formateaMoneda } from "../utils.js";
|
||||
|
||||
class PresupuestoCliente {
|
||||
export default class PresupuestoWizard {
|
||||
|
||||
constructor() {
|
||||
constructor(options = {}) {
|
||||
this.opts = Object.assign({
|
||||
mode: 'public',
|
||||
presupuestoId: null,
|
||||
readonly: false,
|
||||
canSave: false,
|
||||
useSessionCache: true
|
||||
}, options);
|
||||
|
||||
this.DEBUG = true; // Activar o desactivar el modo de depuración
|
||||
|
||||
@ -15,20 +22,22 @@ class PresupuestoCliente {
|
||||
titulo: '',
|
||||
autor: '',
|
||||
isbn: '',
|
||||
tirada1: '',
|
||||
tirada1: 10,
|
||||
tirada2: '',
|
||||
tirada3: '',
|
||||
tirada4: '',
|
||||
ancho: '',
|
||||
alto: '',
|
||||
ancho: 148,
|
||||
alto: 218,
|
||||
formatoPersonalizado: false,
|
||||
paginasNegro: '',
|
||||
paginasColor: '',
|
||||
paginasNegro: 32,
|
||||
paginasColor: 0,
|
||||
posicionPaginasColor: '',
|
||||
tipoEncuadernacion: 'fresado',
|
||||
entregaTipo: 'peninsula',
|
||||
ivaReducido: true,
|
||||
},
|
||||
interior: {
|
||||
tipoImpresion: 'negro',
|
||||
tipoImpresion: 'negrohq',
|
||||
papelInteriorId: 3,
|
||||
gramajeInterior: 80,
|
||||
},
|
||||
@ -61,14 +70,19 @@ class PresupuestoCliente {
|
||||
}
|
||||
},
|
||||
servicios: {
|
||||
servicios: [],
|
||||
servicios: [{
|
||||
"id": "ferro-digital",
|
||||
"label": "Ferro Digital",
|
||||
"price": 0,
|
||||
"units": 1
|
||||
}],
|
||||
datosMarcapaginas: {
|
||||
marcapaginas_tirada: 100,
|
||||
tamanio_marcapaginas: '_50x140_',
|
||||
caras_impresion: 'una_cara',
|
||||
papel_marcapaginas: 'cartulina_grafica',
|
||||
gramaje_marcapaginas: 300,
|
||||
acabado_marcapaginas: 'ninguno',
|
||||
tamanio: '_50x140_',
|
||||
carasImpresion: 'una_cara',
|
||||
papel: 'cartulina_grafica',
|
||||
gramaje: 300,
|
||||
acabado: 'ninguno',
|
||||
resultado: {
|
||||
precio_unitario: 0,
|
||||
precio: 0
|
||||
@ -92,7 +106,7 @@ class PresupuestoCliente {
|
||||
}
|
||||
}
|
||||
},
|
||||
selectedTirada: null,
|
||||
selectedTirada: 10,
|
||||
}
|
||||
|
||||
// pestaña datos generales
|
||||
@ -115,7 +129,9 @@ class PresupuestoCliente {
|
||||
this.divPosicionPaginasColor = $('#div-posicion-paginas-color');
|
||||
this.posicionPaginasColor = $('#posicionPaginasColor');
|
||||
this.paginas = $('#paginas');
|
||||
this.btn_next_datos_generales = $('#next-datos-generales');
|
||||
this.entregaTipo = $('#entregaTipo');
|
||||
this.ivaReducido = $('#iva-reducido');
|
||||
this.btnIvaReducidoDetail = $('#btn-iva-reducido-detail');
|
||||
this.datos_generales_alert = $('#datos-generales-alert');
|
||||
|
||||
// pestaña interior
|
||||
@ -123,7 +139,7 @@ class PresupuestoCliente {
|
||||
this.divOpcionesColor = $('#div-opciones-color');
|
||||
this.divPapelInterior = $('#div-papel-interior');
|
||||
this.divGramajeInterior = $("#div-gramaje-interior");
|
||||
this.interior_alert = $('#interior-alert');
|
||||
this.interior_alert = $('#form-errors');
|
||||
|
||||
// pestaña cubierta
|
||||
this.divSolapasCubierta = $('#div-solapas-cubierta');
|
||||
@ -166,20 +182,59 @@ class PresupuestoCliente {
|
||||
this.summaryTableSobrecubierta = $('#summary-sobrecubierta');
|
||||
this.summaryTableFaja = $('#summary-faja');
|
||||
this.summaryTableExtras = $('#summary-servicios-extras');
|
||||
|
||||
// variable para evitar disparar eventos al cargar datos
|
||||
this._hydrating = false;
|
||||
}
|
||||
|
||||
init() {
|
||||
async init() {
|
||||
|
||||
const stored = sessionStorage.getItem("formData");
|
||||
const self = this;
|
||||
|
||||
$.ajaxSetup({
|
||||
beforeSend: function (xhr) {
|
||||
const token = document.querySelector('meta[name="_csrf"]')?.content;
|
||||
const header = document.querySelector('meta[name="_csrf_header"]')?.content;
|
||||
if (token && header) xhr.setRequestHeader(header, token);
|
||||
}
|
||||
});
|
||||
|
||||
const root = document.getElementById('presupuesto-app');
|
||||
const mode = root?.dataset.mode || 'public';
|
||||
const presupuestoId = this.opts.presupuestoId || null;
|
||||
|
||||
let stored = null;
|
||||
if (this.opts.useSessionCache) {
|
||||
stored = sessionStorage.getItem("formData");
|
||||
}
|
||||
|
||||
this.#initDatosGenerales();
|
||||
this.#initCubierta();
|
||||
|
||||
if (stored) {
|
||||
this.formData = JSON.parse(stored);
|
||||
this.#loadDatosGeneralesData();
|
||||
if (presupuestoId && mode !== 'public') {
|
||||
|
||||
await fetch(`/presupuesto/api/get?id=${encodeURIComponent(presupuestoId)}`, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(dto => {
|
||||
sessionStorage.removeItem("formData");
|
||||
this.formData = dto;
|
||||
this.#cacheFormData();
|
||||
this.#loadDatosGeneralesData();
|
||||
});
|
||||
} else {
|
||||
if (stored) {
|
||||
sessionStorage.removeItem("formData");
|
||||
this.formData = JSON.parse(stored);
|
||||
this.#cacheFormData();
|
||||
this.#loadDatosGeneralesData();
|
||||
}
|
||||
}
|
||||
|
||||
this.#initCubierta();
|
||||
|
||||
Summary.updateEncuadernacion();
|
||||
Summary.updateFormato();
|
||||
Summary.updatePaginas();
|
||||
@ -205,10 +260,131 @@ class PresupuestoCliente {
|
||||
}
|
||||
});
|
||||
|
||||
if (this.opts.canSave) {
|
||||
$('.guardar-presupuesto').on('click', async () => {
|
||||
|
||||
const success = await this.#guardarPresupuesto();
|
||||
if (success) {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: window.languageBundle?.get('presupuesto.exito.guardado') || 'Guardado',
|
||||
timer: 1800,
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
|
||||
cancelButton: 'btn btn-light' // clases para cancelar
|
||||
},
|
||||
showConfirmButton: false
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
$('.add-cart-btn').on('click', async () => {
|
||||
const success = await this.#guardarPresupuesto();
|
||||
if (success) {
|
||||
const res = await $.ajax({
|
||||
url: `/cart/add/${this.opts.presupuestoId}`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
// Si el backend devuelve { redirect: "/cart" }
|
||||
if (res?.redirect) {
|
||||
window.location.assign(res.redirect); // o replace()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// Usa function() para que `this` sea el botón
|
||||
$('.btn-imprimir').on('click', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// obtén el id de donde lo tengas (data-attr o variable global)
|
||||
const id = this.opts.presupuestoId;
|
||||
|
||||
const url = `/api/pdf/presupuesto/${id}?mode=download`;
|
||||
|
||||
// Truco: crear <a> y hacer click
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.target = '_self'; // descarga en la misma pestaña
|
||||
// a.download = `presupuesto-${id}.pdf`; // opcional, tu server ya pone filename
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
async #guardarPresupuesto() {
|
||||
|
||||
const alert = $('#form-errors');
|
||||
const servicios = [];
|
||||
const payload = {
|
||||
id: this.opts.presupuestoId,
|
||||
mode: this.opts.mode,
|
||||
presupuesto: this.#getPresupuestoData(),
|
||||
servicios: this.formData.servicios.servicios,
|
||||
datosMaquetacion: this.formData.servicios.datosMaquetacion,
|
||||
datosMarcapaginas: this.formData.servicios.datosMarcapaginas,
|
||||
cliente_id: $('#cliente_id').val() || null,
|
||||
};
|
||||
|
||||
try {
|
||||
alert.addClass('d-none').find('ul').empty();
|
||||
return await $.ajax({
|
||||
url: '/presupuesto/api/save',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(payload)
|
||||
}).then((data) => {
|
||||
|
||||
if (data.id) this.opts.presupuestoId = data.id;
|
||||
|
||||
return true;
|
||||
}).catch((xhr, status, error) => {
|
||||
|
||||
const errors = xhr.responseJSON;
|
||||
if (errors && typeof errors === 'object') {
|
||||
if (!this.DEBUG && xhr.responseJSON.error && xhr.responseJSON.error == 'Internal Server Error') {
|
||||
console.error("Error al validar los datos generales. Internal Server Error");
|
||||
return;
|
||||
}
|
||||
Object.values(errors).forEach(errorMsg => {
|
||||
alert.find('ul').append(`<li>${errorMsg}</li>`);
|
||||
});
|
||||
alert.removeClass('d-none');
|
||||
} else {
|
||||
alert.find('ul').append('<li>Error desconocido. Por favor, inténtelo de nuevo más tarde.</li>');
|
||||
$(window).scrollTop(0);
|
||||
alert.removeClass('d-none');
|
||||
}
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
return false;
|
||||
|
||||
});
|
||||
} catch (e) {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: window.languageBundle?.get('presupuesto.add.error.save.title') || 'Error',
|
||||
text: e?.message || '',
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
|
||||
cancelButton: 'btn btn-light' // clases para cancelar
|
||||
},
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#cacheFormData() {
|
||||
sessionStorage.setItem("formData", JSON.stringify(this.formData));
|
||||
if (!this.opts.useSessionCache) return;
|
||||
sessionStorage.setItem('formData', JSON.stringify(this.formData));
|
||||
}
|
||||
|
||||
#changeTab(idContenidoTab) {
|
||||
@ -216,6 +392,7 @@ class PresupuestoCliente {
|
||||
if (tabButton) {
|
||||
bootstrap.Tab.getOrCreateInstance(tabButton).show();
|
||||
}
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
#getPresupuestoData() {
|
||||
@ -224,6 +401,8 @@ class PresupuestoCliente {
|
||||
...this.#getInteriorData(),
|
||||
...this.#getCubiertaData(),
|
||||
selectedTirada: this.formData.selectedTirada
|
||||
|
||||
|
||||
};
|
||||
|
||||
const sobrecubierta = data.sobrecubierta;
|
||||
@ -279,6 +458,27 @@ class PresupuestoCliente {
|
||||
******************************/
|
||||
#initDatosGenerales() {
|
||||
|
||||
this.btnIvaReducidoDetail.on('click', () => {
|
||||
Swal.fire({
|
||||
position: 'top-end',
|
||||
icon: 'info',
|
||||
title: window.languageBundle.get('presupuesto.iva-reducido'),
|
||||
html: `
|
||||
<div class="acitivity-timeline p-4">
|
||||
${window.languageBundle.get('presupuesto.iva-reducido-descripcion')}
|
||||
</div>
|
||||
`,
|
||||
confirmButtonClass: 'btn btn-primary w-xs mt-2',
|
||||
showConfirmButton: false,
|
||||
showCloseButton: true,
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
|
||||
cancelButton: 'btn btn-light' // clases para cancelar
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
$('.datos-generales-data').on('change', () => {
|
||||
const dataToStore = this.#getDatosGeneralesData();
|
||||
this.#updateDatosGeneralesData(dataToStore);
|
||||
@ -311,14 +511,15 @@ class PresupuestoCliente {
|
||||
Summary.updateFormato();
|
||||
});
|
||||
|
||||
this.btn_next_datos_generales.on('click', () => {
|
||||
$('.btn-change-tab-datos-generales').on('click', () => {
|
||||
this.#nextDatosGenerales();
|
||||
});
|
||||
|
||||
// Eventos para el resumen
|
||||
$(document).on('change', 'input[name="tipoEncuadernacion"]', (e) => {
|
||||
|
||||
if ($(e.target).is(':checked')) {
|
||||
// Actualizar el resumen
|
||||
|
||||
Summary.updateEncuadernacion();
|
||||
}
|
||||
});
|
||||
@ -381,6 +582,18 @@ class PresupuestoCliente {
|
||||
|
||||
$('.alto-faja-max').text(`max: ${this.formData.datosGenerales.alto} mm`);
|
||||
|
||||
// check if selected tirada is still valid
|
||||
const tiradas = [
|
||||
parseInt(this.formData.datosGenerales.tirada1),
|
||||
parseInt(this.formData.datosGenerales.tirada2) || 0,
|
||||
parseInt(this.formData.datosGenerales.tirada3) || 0,
|
||||
parseInt(this.formData.datosGenerales.tirada4) || 0,
|
||||
].filter(t => t > 0).sort((a, b) => a - b);
|
||||
|
||||
if (!tiradas.includes(this.formData.selectedTirada)) {
|
||||
this.formData.selectedTirada = tiradas[0];
|
||||
}
|
||||
|
||||
this.#loadInteriorData(data);
|
||||
|
||||
const interiorData = this.#getInteriorData();
|
||||
@ -409,7 +622,9 @@ class PresupuestoCliente {
|
||||
paginasNegro: this.paginasNegro.val(),
|
||||
paginasColor: this.paginasColor.val(),
|
||||
posicionPaginasColor: this.posicionPaginasColor.val(),
|
||||
tipoEncuadernacion: ($('.tipo-libro.selected').length > 0) ? $('.tipo-libro.selected').attr('id') : 'fresado',
|
||||
tipoEncuadernacion: $('.tipo-libro input:checked').val() || 'fresado',
|
||||
entregaTipo: this.entregaTipo.val(),
|
||||
ivaReducido: this.ivaReducido.is(':checked'),
|
||||
};
|
||||
}
|
||||
|
||||
@ -430,6 +645,8 @@ class PresupuestoCliente {
|
||||
paginasColor: data.paginasColor,
|
||||
posicionPaginasColor: data.posicionPaginasColor,
|
||||
tipoEncuadernacion: data.tipoEncuadernacion,
|
||||
entregaTipo: data.entregaTipo,
|
||||
ivaReducido: data.ivaReducido,
|
||||
};
|
||||
}
|
||||
|
||||
@ -444,7 +661,7 @@ class PresupuestoCliente {
|
||||
this.tirada4.val(this.formData.datosGenerales.tirada4);
|
||||
|
||||
this.paginasNegro.val(this.formData.datosGenerales.paginasNegro);
|
||||
this.paginasColor.val(this.formData.datosGenerales.paginasColor);
|
||||
this.paginasColor.val(this.formData.datosGenerales.paginasColor);;
|
||||
|
||||
this.posicionPaginasColor.val(this.formData.datosGenerales.posicionPaginasColor);
|
||||
|
||||
@ -454,8 +671,8 @@ class PresupuestoCliente {
|
||||
this.alto.val(this.formData.datosGenerales.alto);
|
||||
}
|
||||
|
||||
$('.tipo-libro').removeClass('selected');
|
||||
$('.image-container#' + this.formData.datosGenerales.tipoEncuadernacion).trigger('click');
|
||||
$('input[name="tipoEncuadernacion"][value="' + this.formData.datosGenerales.tipoEncuadernacion + '"]')
|
||||
.prop('checked', true);
|
||||
this.#updateTipoEncuadernacion();
|
||||
|
||||
this.formatoPersonalizado.trigger('change');
|
||||
@ -475,6 +692,9 @@ class PresupuestoCliente {
|
||||
this.formato.val(option.val()).trigger('change');
|
||||
}
|
||||
}
|
||||
|
||||
this.entregaTipo.val(this.formData.datosGenerales.entregaTipo);
|
||||
this.ivaReducido.prop('checked', this.formData.datosGenerales.ivaReducido);
|
||||
}
|
||||
|
||||
#getTamanio() {
|
||||
@ -499,7 +719,7 @@ class PresupuestoCliente {
|
||||
#updateTipoEncuadernacion() {
|
||||
|
||||
const paginas = parseInt($('#paginas').val());
|
||||
const selectedTipo = $('.tipo-libro.selected').attr('id');
|
||||
const selectedTipo = $('.tipo-libro input:checked').val();
|
||||
$('.tipo-libro').removeClass('selected');
|
||||
|
||||
if (paginas < 32) {
|
||||
@ -525,19 +745,17 @@ class PresupuestoCliente {
|
||||
$('.tipo-libro#grapado').removeClass('d-none');
|
||||
}
|
||||
|
||||
if (selectedTipo && $('.tipo-libro#' + selectedTipo).length > 0 && !$('.tipo-libro#' + selectedTipo).hasClass('d-none')) {
|
||||
$('.tipo-libro#' + selectedTipo).addClass('selected');
|
||||
}
|
||||
else {
|
||||
if (!(selectedTipo && $(`.tipo-libro input[value="${selectedTipo}"]`).length && !$(`.tipo-libro input[value="${selectedTipo}"]`).closest('.tipo-libro').hasClass('d-none'))) {
|
||||
|
||||
let firstVisible = $('.tipo-libro').not('.d-none').first();
|
||||
|
||||
if (firstVisible.length) {
|
||||
firstVisible.addClass('selected');
|
||||
firstVisible.trigger('click');
|
||||
}
|
||||
}
|
||||
|
||||
if ($('.tipo-libro.selected').length > 0) {
|
||||
this.formData.datosGenerales.tipoEncuadernacion = $('.tipo-libro.selected').attr('id');
|
||||
if ($('.tipo-libro input:checked').length > 0) {
|
||||
this.formData.datosGenerales.tipoEncuadernacion = $('.tipo-libro input:checked').val();
|
||||
Summary.updateEncuadernacion();
|
||||
}
|
||||
else {
|
||||
@ -562,7 +780,9 @@ class PresupuestoCliente {
|
||||
});
|
||||
|
||||
|
||||
$(document).on('click', '.opcion-color', (e) => {
|
||||
$(document).on('change', 'input[name="tipoImpresion"]', (e) => {
|
||||
if (!$(e.target).is(':checked'))
|
||||
return;
|
||||
|
||||
const data = this.#getPresupuestoData();
|
||||
Summary.updateTipoImpresion();
|
||||
@ -590,6 +810,7 @@ class PresupuestoCliente {
|
||||
if (opcion.id === this.formData.interior.papelInteriorId) {
|
||||
item.setSelected(true);
|
||||
}
|
||||
item.group = 'papel-interior';
|
||||
this.divPapelInterior.append(item.render());
|
||||
}
|
||||
|
||||
@ -613,10 +834,16 @@ class PresupuestoCliente {
|
||||
});
|
||||
});
|
||||
|
||||
$(document).on('click', '.papel-interior', (e) => {
|
||||
|
||||
$(document).on('change', 'input[name="papelInterior"]', (e) => {
|
||||
|
||||
if (!$(e.target).is(':checked'))
|
||||
return;
|
||||
|
||||
const data = this.#getPresupuestoData();
|
||||
|
||||
this.interior_alert.addClass('d-none').find('#form-errors-alert-list').empty();
|
||||
|
||||
Summary.updatePapelInterior();
|
||||
|
||||
this.divGramajeInterior.removeClass('animate-fadeInUpBounce');
|
||||
@ -641,9 +868,16 @@ class PresupuestoCliente {
|
||||
const dataInterior = this.#getInteriorData();
|
||||
this.#updateInteriorData(dataInterior);
|
||||
this.#cacheFormData();
|
||||
Summary.updatePapelInterior();
|
||||
Summary.updateGramajeInterior();
|
||||
|
||||
}).fail((xhr, status, error) => {
|
||||
this.interior_alert.removeClass('d-none');
|
||||
const errors = xhr.responseJSON;
|
||||
if (errors && typeof errors === 'object') {
|
||||
this.interior_alert.find('#form-errors-alert-list').append(`<li>${errors.message}</li>`);
|
||||
|
||||
}
|
||||
console.error("Error al obtener los gramajes de interior: ", xhr.responseText);
|
||||
});
|
||||
});
|
||||
@ -679,14 +913,15 @@ class PresupuestoCliente {
|
||||
$('.btn-change-tab-interior').on('click', (e) => {
|
||||
|
||||
let data = this.#getPresupuestoData();
|
||||
const id = e.currentTarget.id;
|
||||
const action = $(e.currentTarget).data('btn-action') || 'next';
|
||||
this.interior_alert.addClass('d-none').find('#form-errors-alert-list').empty();
|
||||
|
||||
$.ajax({
|
||||
url: '/presupuesto/public/validar/interior',
|
||||
type: 'POST',
|
||||
data: data,
|
||||
success: (data) => {
|
||||
if (id === 'btn-prev-interior') {
|
||||
if (action === 'previous') {
|
||||
this.summaryTableInterior.addClass('d-none');
|
||||
this.#changeTab('pills-general-data');
|
||||
} else {
|
||||
@ -737,18 +972,16 @@ class PresupuestoCliente {
|
||||
error: (xhr, status, error) => {
|
||||
|
||||
this.interior_alert.removeClass('d-none');
|
||||
this.interior_alert.find('#inside-alert-list').empty();
|
||||
this.interior_alert.find('#form-errors-alert-list').empty();
|
||||
const errors = xhr.responseJSON;
|
||||
if (errors && typeof errors === 'object') {
|
||||
if (!this.DEBUG && xhr.responseJSON.error && xhr.responseJSON.error == 'Internal Server Error') {
|
||||
console.error("Error al validar los datos generales. Internal Server Error");
|
||||
return;
|
||||
}
|
||||
Object.values(errors).forEach(errorMsg => {
|
||||
this.interior_alert.find('#inside-alert-list').append(`<li>${errorMsg}</li>`);
|
||||
});
|
||||
this.interior_alert.find('#form-errors-alert-list').append(`<li>${errors.message}</li>`);
|
||||
} else {
|
||||
this.interior_alert.find('#inside-alert-list').append('<li>Error desconocido. Por favor, inténtelo de nuevo más tarde.</li>');
|
||||
this.interior_alert.find('#form-errors-alert-list').append('<li>Error desconocido. Por favor, inténtelo de nuevo más tarde.</li>');
|
||||
}
|
||||
$(window).scrollTop(0);
|
||||
}
|
||||
@ -769,6 +1002,7 @@ class PresupuestoCliente {
|
||||
for (let i = 0; i < opciones_color.length; i++) {
|
||||
const opcion = opciones_color[i];
|
||||
const item = new imagen_presupuesto(opcion);
|
||||
item.group = 'tipoImpresion';
|
||||
item.extraClass = 'interior-data opcion-color';
|
||||
if ((this.formData.interior.tipoImpresion === '' && i === 0) ||
|
||||
this.formData.interior.tipoImpresion === opcion.id) {
|
||||
@ -781,6 +1015,7 @@ class PresupuestoCliente {
|
||||
for (let i = 0; i < opciones_papel_interior.length; i++) {
|
||||
const opcion = opciones_papel_interior[i];
|
||||
const item = new imagen_presupuesto(opcion);
|
||||
item.group = 'papelInterior';
|
||||
item.extraClass = 'interior-data papel-interior';
|
||||
if (this.formData.interior.papelInteriorId == '' && i === 0 ||
|
||||
this.formData.interior.papelInteriorId == opcion.extra_data["sk-id"]) {
|
||||
@ -822,8 +1057,8 @@ class PresupuestoCliente {
|
||||
|
||||
#getInteriorData() {
|
||||
|
||||
const tipoImpresion = $('#div-opciones-color .image-container.selected').attr('id') || 'negro';
|
||||
const papelInteriorId = $('#div-papel-interior .image-container.selected').data('sk-id') || 3;
|
||||
const tipoImpresion = $('#div-opciones-color .image-container input:checked').parent().attr('id') || 'negro';
|
||||
const papelInteriorId = $('#div-papel-interior .image-container input:checked').parent().data('sk-id') || 3;
|
||||
const gramajeInterior = $('input[name="gramaje-interior"]:checked').data('gramaje') || 90;
|
||||
return {
|
||||
tipoImpresion: tipoImpresion,
|
||||
@ -865,7 +1100,12 @@ class PresupuestoCliente {
|
||||
`,
|
||||
confirmButtonClass: 'btn btn-primary w-xs mt-2',
|
||||
showConfirmButton: false,
|
||||
showCloseButton: true
|
||||
showCloseButton: true,
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
|
||||
cancelButton: 'btn btn-light' // clases para cancelar
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@ -877,16 +1117,27 @@ class PresupuestoCliente {
|
||||
html: window.languageBundle.get('presupuesto.impresion-cubierta-help'),
|
||||
confirmButtonClass: 'btn btn-primary w-xs mt-2',
|
||||
showConfirmButton: false,
|
||||
showCloseButton: true
|
||||
showCloseButton: true,
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
|
||||
cancelButton: 'btn btn-light' // clases para cancelar
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
$(document).on('click', '.tapa-cubierta', (e) => {
|
||||
$(document).on('change', 'input[name="tipoCubierta"]', (e) => {
|
||||
|
||||
if (!$(e.target).is(':checked'))
|
||||
return;
|
||||
|
||||
if (this._hydrating)
|
||||
return;
|
||||
|
||||
$('.tapa-dura-options').eq(0).removeClass('animate-fadeInUpBounce');
|
||||
$('.tapa-blanda-options').eq(0).removeClass('animate-fadeInUpBounce');
|
||||
|
||||
if (e.currentTarget.id === 'tapaBlanda') {
|
||||
if (e.currentTarget.value === 'tapaBlanda') {
|
||||
$('.tapa-blanda-options').eq(0).addClass('animate-fadeInUpBounce');
|
||||
$('.tapa-dura-options').addClass('d-none');
|
||||
$('.tapa-blanda-options').removeClass('d-none');
|
||||
@ -897,13 +1148,16 @@ class PresupuestoCliente {
|
||||
$('.tapa-dura-options').removeClass('d-none');
|
||||
}
|
||||
|
||||
this.#getPapelesCubierta(e.currentTarget.id);
|
||||
this.#getPapelesCubierta(e.currentTarget.value);
|
||||
Summary.updateTapaCubierta();
|
||||
});
|
||||
|
||||
$(document).on('click', '.solapas-cubierta', (e) => {
|
||||
$(document).on('change', 'input[name="solapasCubierta"]', (e) => {
|
||||
|
||||
if (e.currentTarget.id === 'sin-solapas') {
|
||||
if (!$(e.target).is(':checked'))
|
||||
return;
|
||||
|
||||
if (e.currentTarget.closest('.image-container').id === 'sin-solapas') {
|
||||
this.divSolapasCubierta.addClass('d-none');
|
||||
}
|
||||
else {
|
||||
@ -917,7 +1171,8 @@ class PresupuestoCliente {
|
||||
Summary.updateTapaCubierta();
|
||||
});
|
||||
|
||||
$(document).on('click', '.papel-cubierta', (e) => {
|
||||
|
||||
$(document).on('change', 'input[name="papel-cubierta"]', (e) => {
|
||||
|
||||
const data = this.#getPresupuestoData();
|
||||
|
||||
@ -931,6 +1186,7 @@ class PresupuestoCliente {
|
||||
}).done((data) => {
|
||||
|
||||
const gramajes = data.opciones_gramaje_cubierta;
|
||||
this.divGramajeCubierta.empty();
|
||||
this.#addGramajesCubierta(gramajes);
|
||||
this.divGramajeCubierta.addClass('animate-fadeInUpBounce');
|
||||
|
||||
@ -939,6 +1195,7 @@ class PresupuestoCliente {
|
||||
this.#cacheFormData();
|
||||
|
||||
Summary.updatePapelCubierta();
|
||||
|
||||
Summary.updateGramajeCubierta();
|
||||
|
||||
}).fail((xhr, status, error) => {
|
||||
@ -952,11 +1209,14 @@ class PresupuestoCliente {
|
||||
const gramaje = parseInt($('#' + inputId).data('gramaje'));
|
||||
this.formData.cubierta.gramajeCubierta = gramaje;
|
||||
this.#cacheFormData();
|
||||
Summary.updateGramajeCubierta();
|
||||
Summary.updateGramajeCubierta(gramaje);
|
||||
});
|
||||
|
||||
$(document).on('change', '.datos-cubierta', (e) => {
|
||||
|
||||
if (this._hydrating)
|
||||
return;
|
||||
|
||||
const dataToStore = this.#getCubiertaData();
|
||||
this.#updateCubiertaData(dataToStore);
|
||||
this.#cacheFormData();
|
||||
@ -1017,9 +1277,9 @@ class PresupuestoCliente {
|
||||
$('.btn-change-tab-cubierta').on('click', (e) => {
|
||||
|
||||
const data = this.#getPresupuestoData();
|
||||
const id = e.currentTarget.id;
|
||||
const action = $(e.currentTarget).data('btn-action') || 'next';
|
||||
|
||||
if (id === 'btn-prev-cubierta') {
|
||||
if (action === 'previous') {
|
||||
data.calcular = false;
|
||||
}
|
||||
else {
|
||||
@ -1031,7 +1291,7 @@ class PresupuestoCliente {
|
||||
type: 'POST',
|
||||
data: data,
|
||||
success: (data) => {
|
||||
if (id === 'btn-prev-cubierta') {
|
||||
if (action === 'previous') {
|
||||
this.summaryTableCubierta.addClass('d-none');
|
||||
this.#changeTab('pills-inside');
|
||||
}
|
||||
@ -1042,7 +1302,7 @@ class PresupuestoCliente {
|
||||
}
|
||||
},
|
||||
error: (xhr, status, error) => {
|
||||
if (id === 'btn-prev-cubierta') {
|
||||
if (action === 'previous') {
|
||||
this.#changeTab('pills-inside');
|
||||
return;
|
||||
}
|
||||
@ -1096,13 +1356,13 @@ class PresupuestoCliente {
|
||||
if (item.extraData["sk-id"] == this.formData.cubierta.papelCubiertaId) {
|
||||
item.setSelected(true);
|
||||
}
|
||||
item.group = 'papel-cubierta';
|
||||
this.divPapelCubierta.append(item.render());
|
||||
}
|
||||
|
||||
if (this.divPapelCubierta.find('.image-container.selected').length === 0) {
|
||||
this.divPapelCubierta.find('.image-container').first().addClass('selected');
|
||||
this.formData.cubierta.papelCubiertaId =
|
||||
this.divPapelCubierta.find('.image-container').first().data('sk-id') || 3;
|
||||
if (this.divPapelCubierta.find('input[name="papel-cubierta"]:checked').length === 0) {
|
||||
|
||||
this.divPapelCubierta.find('input[name="papel-cubierta"]').first().prop('checked', true).trigger('change');
|
||||
}
|
||||
|
||||
this.#addGramajesCubierta(data.opciones_gramaje_cubierta);
|
||||
@ -1123,15 +1383,15 @@ class PresupuestoCliente {
|
||||
|
||||
#getCubiertaData() {
|
||||
|
||||
const tipoCubierta = $('.tapa-cubierta.selected').attr('id') || 'tapaBlanda';
|
||||
const solapas = $('.solapas-cubierta.selected').id == 'sin-solapas' ? 0 : 1 || 0;
|
||||
const tipoCubierta = $('.tapa-cubierta input:checked').val() || 'tapaBlanda';
|
||||
const solapas = $('.solapas-cubierta input:checked').val() == 'sin-solapas' ? 0 : 1 || 0;
|
||||
const tamanioSolapasCubierta = $('#tamanio-solapas-cubierta').val() || '80';
|
||||
const cubiertaCaras = parseInt(this.carasImpresionCubierta.val()) || 2;
|
||||
const papelGuardasId = parseInt($('#papel-guardas option:selected').data('papel-id')) || 3;
|
||||
const gramajeGuardas = parseInt($('#papel-guardas option:selected').data('gramaje')) || 170;
|
||||
const guardasImpresas = parseInt(this.guardasImpresas) || 0;
|
||||
const guardasImpresas = parseInt(this.guardasImpresas.val()) || 0;
|
||||
const cabezada = this.cabezada.val() || 'WHI';
|
||||
const papelCubiertaId = $('#div-papel-cubierta .image-container.selected').data('sk-id') || this.formData.cubierta.papelCubiertaId || 3;
|
||||
const papelCubiertaId = $('#div-papel-cubierta .image-container input:checked').parent().data('sk-id') || this.formData.cubierta.papelCubiertaId || 3;
|
||||
const gramajeCubierta = $('input[name="gramaje-cubierta"]:checked').data('gramaje') || this.formData.cubierta.gramajeCubierta || 170;
|
||||
const acabado = parseInt($(`input[name="acabado-cubierta"]:checked`).attr('sk-id')) || 1;
|
||||
const sobrecubierta = this.sobrecubierta.hasClass('active');
|
||||
@ -1210,7 +1470,7 @@ class PresupuestoCliente {
|
||||
|
||||
for (let i = 0; i < gramajes.length; i++) {
|
||||
const gramaje = gramajes[i];
|
||||
this.#addGramaje(this.divGramajeCubierta, 'gramaje-cubierta datos-cubierta', gramaje, 'gramaje-cubierta');
|
||||
this.#addGramaje(this.divGramajeCubierta, 'gramaje-cubierta', gramaje, 'gramaje-cubierta');
|
||||
|
||||
// Seleccionar el gramaje por defecto
|
||||
if (this.formData.cubierta.gramajeCubierta === '' && i === 0) {
|
||||
@ -1229,8 +1489,10 @@ class PresupuestoCliente {
|
||||
|
||||
#loadCubiertaData() {
|
||||
|
||||
$('.tapa-cubierta').removeClass('selected');
|
||||
$(`#${this.formData.cubierta.tipoCubierta}`).addClass('selected');
|
||||
this._hydrating = true;
|
||||
|
||||
$('input[name="tipoCubierta"][value="' + this.formData.cubierta.tipoCubierta + '"]')
|
||||
.prop('checked', true);
|
||||
|
||||
if (this.formData.cubierta.tipoCubierta === 'tapaBlanda') {
|
||||
$('.tapa-blanda-options').removeClass('d-none');
|
||||
@ -1246,15 +1508,16 @@ class PresupuestoCliente {
|
||||
this.cabezada.val(this.formData.cubierta.cabezada);
|
||||
}
|
||||
|
||||
$(`#${this.formData.cubierta.tipoCubierta}`).trigger('click');
|
||||
this._hydrating = false;
|
||||
|
||||
$('input[name="tipoCubierta"][value="' + this.formData.cubierta.tipoCubierta + '"]').trigger('change');
|
||||
|
||||
if (this.formData.cubierta.solapasCubierta === 0) {
|
||||
$('.solapas-cubierta#sin-solapas').addClass('selected');
|
||||
$('.solapas-cubierta#sin-solapas input').prop('checked', true);
|
||||
this.divSolapasCubierta.addClass('d-none');
|
||||
}
|
||||
else {
|
||||
$('.solapas-cubierta').removeClass('selected');
|
||||
$(`.solapas-cubierta#con-solapas`).addClass('selected');
|
||||
$('.solapas-cubierta#con-solapas input').prop('checked', true);
|
||||
this.divSolapasCubierta.removeClass('d-none');
|
||||
this.carasImpresionCubierta.val(this.formData.cubierta.cubiertaCaras);
|
||||
this.tamanioSolapasCubierta.val(this.formData.cubierta.tamanioSolapasCubierta);
|
||||
@ -1307,9 +1570,9 @@ class PresupuestoCliente {
|
||||
|
||||
$(document).on('click', '.btn-change-tab-seleccion-tirada', (e) => {
|
||||
|
||||
const id = e.currentTarget.id;
|
||||
const action = $(e.currentTarget).data('btn-action') || 'next';
|
||||
|
||||
if (id === 'btn-prev-seleccion-tirada') {
|
||||
if (action === 'previous') {
|
||||
this.#changeTab('pills-cover');
|
||||
} else {
|
||||
const data = this.#getPresupuestoData();
|
||||
@ -1388,9 +1651,9 @@ class PresupuestoCliente {
|
||||
|
||||
$(document).on('click', '.btn-change-tab-extras', (e) => {
|
||||
|
||||
const id = e.currentTarget.id;
|
||||
const action = $(e.currentTarget).data('btn-action') || 'next';
|
||||
|
||||
if (id === 'btn-prev-extras') {
|
||||
if (action === 'previous') {
|
||||
this.#changeTab('pills-seleccion-tirada');
|
||||
this.summaryTableExtras.addClass('d-none');
|
||||
} else {
|
||||
@ -1398,17 +1661,32 @@ class PresupuestoCliente {
|
||||
const servicios = [];
|
||||
$('.service-checkbox:checked').each(function () {
|
||||
const $servicio = $(this);
|
||||
let price = 0;
|
||||
if ($servicio.attr('id') === 'marcapaginas') {
|
||||
price = self.formData.servicios.datosMarcapaginas.resultado.precio;
|
||||
}
|
||||
else if ($servicio.attr('id') === 'maquetacion') {
|
||||
price = self.formData.servicios.datosMaquetacion.resultado.precio;
|
||||
}
|
||||
else {
|
||||
price = $servicio.data('price') ?? $(`label[for="${$servicio.attr('id')}"] .service-price`).text().trim().replace(" " + self.divExtras.data('currency'), '');
|
||||
}
|
||||
|
||||
servicios.push({
|
||||
id: $servicio.attr('id') ?? $(`label[for="${$servicio.attr('id')}"] .service-title`).text().trim(),
|
||||
label: $(`label[for="${$servicio.attr('id')}"] .service-title`).text().trim(),
|
||||
units: $servicio.attr('id') === 'marcapaginas' ? self.formData.servicios.datosMarcapaginas.marcapaginas_tirada : 1,
|
||||
price: $servicio.data('price') ?? $(`label[for="${$servicio.attr('id')}"] .service-price`).text().trim().replace(" " + self.divExtras.data('currency'), ''),
|
||||
price: price,
|
||||
});
|
||||
});
|
||||
|
||||
const body = {
|
||||
presupuesto: this.#getPresupuestoData(),
|
||||
servicios: servicios
|
||||
save: !this.opts.canSave,
|
||||
mode: this.opts.mode,
|
||||
servicios: servicios,
|
||||
datosMaquetacion: this.formData.servicios.datosMaquetacion,
|
||||
datosMarcapaginas: this.formData.servicios.datosMarcapaginas,
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
@ -1418,6 +1696,12 @@ class PresupuestoCliente {
|
||||
data: JSON.stringify(body)
|
||||
}).then((data) => {
|
||||
$('#resumen-titulo').text(data.titulo);
|
||||
if (data.presupuesto_id) {
|
||||
window.PRESUPUESTO_ID = data.presupuesto_id;
|
||||
}
|
||||
body.presupuesto.id = window.PRESUPUESTO_ID || body.presupuesto.id || null;
|
||||
this.opts.presupuestoId = window.PRESUPUESTO_ID;
|
||||
|
||||
this.#updateResumenTable(data);
|
||||
}).catch((error) => {
|
||||
console.error("Error obtener resumen: ", error);
|
||||
@ -1433,9 +1717,25 @@ class PresupuestoCliente {
|
||||
const $target = $(e.currentTarget);
|
||||
|
||||
if ($target.prop('checked')) {
|
||||
this.formData.servicios.servicios.push($target.val());
|
||||
let price = 0;
|
||||
if ($target.attr('id') === 'marcapaginas') {
|
||||
price = self.formData.servicios.datosMarcapaginas.resultado.precio;
|
||||
}
|
||||
else if ($target.attr('id') === 'maquetacion') {
|
||||
price = self.formData.servicios.datosMaquetacion.resultado.precio;
|
||||
}
|
||||
else {
|
||||
price = $target.data('price') ?? $(`label[for="${$target.attr('id')}"] .service-price`).text().trim().replace(" " + self.divExtras.data('currency'), '');
|
||||
}
|
||||
this.formData.servicios.servicios.push(
|
||||
{
|
||||
id: $target.attr('id') ?? $(`label[for="${$target.attr('id')}"] .service-title`).text().trim(),
|
||||
label: $(`label[for="${$target.attr('id')}"] .service-title`).text().trim(),
|
||||
units: $target.attr('id') === 'marcapaginas' ? self.formData.servicios.datosMarcapaginas.marcapaginas_tirada : 1,
|
||||
price: price,
|
||||
});
|
||||
} else {
|
||||
const index = this.formData.servicios.servicios.indexOf($target.val());
|
||||
const index = this.formData.servicios.servicios.findIndex(item => item.id == $target.attr('id'));
|
||||
if (index > -1) {
|
||||
this.formData.servicios.servicios.splice(index, 1);
|
||||
}
|
||||
@ -1456,10 +1756,10 @@ class PresupuestoCliente {
|
||||
|
||||
this.divExtras.addClass('animate-fadeInUpBounce');
|
||||
for (const extra of servicios) {
|
||||
if (this.formData.servicios.servicios.includes(extra.id) && !extra.checked) {
|
||||
if (this.formData.servicios.servicios.some(s => s.id === extra.id) && !extra.checked) {
|
||||
extra.checked = true;
|
||||
if (extra.id === "marcapaginas" || extra.id === "maquetacion") {
|
||||
extra.price = extra.id === "marcapaginas" ?
|
||||
extra.price = (extra.id === "marcapaginas") ?
|
||||
this.formData.servicios.datosMarcapaginas.resultado.precio :
|
||||
this.formData.servicios.datosMaquetacion.resultado.precio;
|
||||
extra.priceUnit = this.divExtras.data('currency');
|
||||
@ -1467,13 +1767,16 @@ class PresupuestoCliente {
|
||||
}
|
||||
const item = new ServiceOptionCard(extra);
|
||||
this.divExtras.append(item.render());
|
||||
if (item.checked) {
|
||||
if (!this.formData.servicios.servicios.includes(extra.id)) {
|
||||
this.formData.servicios.servicios.push(extra.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.formData.servicios.servicios.some(s => s.id === "ferro-digital")) {
|
||||
this.formData.servicios.servicios.push({
|
||||
id: "ferro-digital",
|
||||
label: "Ferro Digital",
|
||||
units: 1,
|
||||
price: 0
|
||||
});
|
||||
}
|
||||
this.#cacheFormData();
|
||||
Summary.updateExtras();
|
||||
}
|
||||
@ -1500,8 +1803,14 @@ class PresupuestoCliente {
|
||||
...result,
|
||||
};
|
||||
|
||||
const list = this.formData.servicios.servicios;
|
||||
if (!list.includes('maquetacion')) list.push('maquetacion');
|
||||
if (!this.formData.servicios.servicios.some(s => s.id === "maquetacion") && result.precio > 0) {
|
||||
this.formData.servicios.servicios.push({
|
||||
id: "maquetacion",
|
||||
label: $(`label[for="maquetacion"] .service-title`).text().trim(),
|
||||
units: 1,
|
||||
price: result.precio,
|
||||
});
|
||||
}
|
||||
|
||||
this.#cacheFormData();
|
||||
});
|
||||
@ -1530,10 +1839,6 @@ class PresupuestoCliente {
|
||||
...result,
|
||||
};
|
||||
|
||||
// asegúrate de añadir el servicio seleccionado
|
||||
const list = this.formData.servicios.servicios;
|
||||
if (!list.includes('marcapaginas')) list.push('marcapaginas');
|
||||
|
||||
this.#cacheFormData();
|
||||
});
|
||||
|
||||
@ -1562,7 +1867,8 @@ class PresupuestoCliente {
|
||||
|
||||
// 2) Botón "atrás" en Resumen
|
||||
$(document).on('click', '.btn-change-tab-resumen', (e) => {
|
||||
if (e.currentTarget.id === 'btn-prev-resumen') {
|
||||
const action = $(e.currentTarget).data('btn-action') || 'next';
|
||||
if (action === 'previous') {
|
||||
this.#changeTab('pills-extras');
|
||||
}
|
||||
});
|
||||
@ -1584,8 +1890,6 @@ class PresupuestoCliente {
|
||||
});
|
||||
const servicios = data.servicios || [];
|
||||
|
||||
let total = 0;
|
||||
|
||||
const locale = document.documentElement.lang || 'es-ES';
|
||||
|
||||
for (const l of lineas) {
|
||||
@ -1598,7 +1902,6 @@ class PresupuestoCliente {
|
||||
<td class="text-end">${formateaMoneda(data[l].precio_total, 2, locale)}</td>
|
||||
</tr>
|
||||
`;
|
||||
total += data[l].precio_total;
|
||||
this.tablaResumen.find('tbody').append(row);
|
||||
}
|
||||
for (const s of servicios) {
|
||||
@ -1611,13 +1914,26 @@ class PresupuestoCliente {
|
||||
<td class="text-end">${s.id === "marcapaginas" ? formateaMoneda(s.precio * s.unidades, 2, locale) : formateaMoneda(s.precio, 2, locale)}</td>
|
||||
</tr>
|
||||
`;
|
||||
total += s.precio;
|
||||
this.tablaResumen.find('tbody').append(row);
|
||||
}
|
||||
|
||||
$('#resumen-base').text(formateaMoneda(total, 2, locale));
|
||||
$('#resumen-iva').text(formateaMoneda(total * 0.04, 2, locale));
|
||||
$('#resumen-total').text(formateaMoneda(total * 1.04, 2, locale));
|
||||
$('#resumen-base').text(formateaMoneda(data.base_imponible, 2, locale));
|
||||
if (data.iva_importe_4 > 0) {
|
||||
$('#tr-resumen-iva4').removeClass('d-none');
|
||||
$('#resumen-iva4').text(formateaMoneda(data.iva_importe_4, 2, locale));
|
||||
}
|
||||
else {
|
||||
$('#tr-resumen-iva4').addClass('d-none');
|
||||
$('#resumen-iva4').text(formateaMoneda(0, 2, locale));
|
||||
}
|
||||
if (data.iva_importe_21 > 0) {
|
||||
$('#tr-resumen-iva21').removeClass('d-none');
|
||||
$('#resumen-iva21').text(formateaMoneda(data.iva_importe_21, 2, locale));
|
||||
} else {
|
||||
$('#tr-resumen-iva21').addClass('d-none');
|
||||
$('#resumen-iva21').text(formateaMoneda(0, 2, locale));
|
||||
}
|
||||
$('#resumen-total').text(formateaMoneda(data.total_con_iva, 2, locale));
|
||||
}
|
||||
|
||||
/******************************
|
||||
@ -1626,7 +1942,3 @@ class PresupuestoCliente {
|
||||
}
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const presupuestoCliente = new PresupuestoCliente();
|
||||
presupuestoCliente.init();
|
||||
});
|
||||
@ -0,0 +1,151 @@
|
||||
(() => {
|
||||
// si jQuery está cargado, añade CSRF a AJAX
|
||||
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
|
||||
const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.getAttribute('content');
|
||||
if (window.$ && csrfToken && csrfHeader) {
|
||||
$.ajaxSetup({
|
||||
beforeSend: function (xhr) {
|
||||
xhr.setRequestHeader(csrfHeader, csrfToken);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const language = document.documentElement.lang || 'es-ES';
|
||||
|
||||
// Comprueba dependencias antes de iniciar
|
||||
if (!window.DataTable) {
|
||||
console.error('DataTables no está cargado aún');
|
||||
return;
|
||||
}
|
||||
|
||||
const table = new DataTable('#presupuestos-clientes-user-datatable', {
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
orderCellsTop: true,
|
||||
pageLength: 50,
|
||||
lengthMenu: [10, 25, 50, 100, 500],
|
||||
language: { url: '/assets/libs/datatables/i18n/' + language + '.json' },
|
||||
responsive: true,
|
||||
dom: 'lBrtip',
|
||||
buttons: {
|
||||
dom: {
|
||||
button: {
|
||||
className: 'btn btn-sm btn-outline-primary me-1'
|
||||
},
|
||||
buttons: [
|
||||
{ extend: 'copy' },
|
||||
{ extend: 'csv' },
|
||||
{ extend: 'excel' },
|
||||
{ extend: 'pdf' },
|
||||
{ extend: 'print' },
|
||||
{ extend: 'colvis' }
|
||||
],
|
||||
}
|
||||
},
|
||||
ajax: {
|
||||
url: '/presupuesto/datatable/clientes',
|
||||
method: 'GET',
|
||||
},
|
||||
order: [[0, 'asc']],
|
||||
columns: [
|
||||
{ data: 'id', name: 'id', orderable: true },
|
||||
{ data: 'titulo', name: 'titulo', orderable: true },
|
||||
{ data: 'tipoEncuadernacion', name: 'tipoEncuadernacion', orderable: true },
|
||||
{ data: 'tipoCubierta', name: 'tipoCubierta', orderable: true },
|
||||
{ data: 'tipoImpresion', name: 'tipoImpresion', orderable: true },
|
||||
{ data: 'selectedTirada', name: 'selectedTirada', orderable: true },
|
||||
{ data: 'paginas', name: 'paginas', orderable: true },
|
||||
{ data: 'estado', name: 'estado', orderable: true },
|
||||
{ data: 'totalConIva', name: 'totalConIva', orderable: true },
|
||||
{ data: 'updatedAt', name: 'updatedAt', orderable: true },
|
||||
{ data: 'actions', orderable: false, searchable: false }
|
||||
],
|
||||
});
|
||||
|
||||
$('#presupuestos-clientes-user-datatable').on('click', '.btn-edit-privado', function (e) {
|
||||
|
||||
e.preventDefault();
|
||||
const id = $(this).data('id');
|
||||
if (id) {
|
||||
window.location.href = '/presupuesto/edit/' + id;
|
||||
}
|
||||
});
|
||||
|
||||
$('#presupuestos-clientes-user-datatable').on('click', '.btn-delete-privado', function (e) {
|
||||
|
||||
e.preventDefault();
|
||||
const id = $(this).data('id');
|
||||
|
||||
Swal.fire({
|
||||
title: window.languageBundle.get(['presupuesto.delete.title']) || 'Eliminar presupuesto',
|
||||
html: window.languageBundle.get(['presupuesto.delete.text']) || 'Esta acción no se puede deshacer.',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-danger w-xs mt-2',
|
||||
cancelButton: 'btn btn-light w-xs mt-2'
|
||||
},
|
||||
confirmButtonText: window.languageBundle.get(['presupuesto.delete.button']) || 'Eliminar',
|
||||
cancelButtonText: window.languageBundle.get(['app.cancelar']) || 'Cancelar',
|
||||
}).then((result) => {
|
||||
if (!result.isConfirmed) return;
|
||||
|
||||
$.ajax({
|
||||
url: '/presupuesto/' + id,
|
||||
type: 'DELETE',
|
||||
success: function () {
|
||||
Swal.fire({
|
||||
icon: 'success', title: window.languageBundle.get(['presupuesto.delete.ok.title']) || 'Eliminado',
|
||||
text: window.languageBundle.get(['presupuesto.delete.ok.text']) || 'El presupuesto ha sido eliminado con éxito.',
|
||||
showConfirmButton: true,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary w-xs mt-2',
|
||||
},
|
||||
});
|
||||
$('#presupuestos-anonimos-datatable').DataTable().ajax.reload(null, false);
|
||||
},
|
||||
error: function (xhr) {
|
||||
// usa el mensaje del backend; fallback genérico por si no llega JSON
|
||||
const msg = (xhr.responseJSON && xhr.responseJSON.message)
|
||||
|| 'Error al eliminar el presupuesto.';
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'No se pudo eliminar',
|
||||
text: msg,
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
|
||||
cancelButton: 'btn btn-light' // clases para cancelar
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$('#presupuestos-clientes-user-datatable').on('keyup', '.presupuesto-filter', function (e) {
|
||||
const colName = $(this).data('col');
|
||||
const colIndex = table.column(colName + ':name').index();
|
||||
|
||||
if (table.column(colIndex).search() !== this.value) {
|
||||
table.column(colIndex).search(this.value).draw();
|
||||
}
|
||||
});
|
||||
|
||||
$('#presupuestos-clientes-user-datatable').on('change', '.presupuesto-select-filter', function (e) {
|
||||
const name = $(this).data('col');
|
||||
const colIndex = table.column(name + ':name').index();
|
||||
|
||||
if (table.column(colIndex).search() !== this.value) {
|
||||
table.column(colIndex).search(this.value).draw();
|
||||
}
|
||||
});
|
||||
|
||||
$('#addPresupuestoButton').on('click', async function (e) {
|
||||
|
||||
e.preventDefault();
|
||||
window.location.href = '/presupuesto/add/private/' + $('#cliente_id').val();
|
||||
});
|
||||
|
||||
})();
|
||||
@ -0,0 +1,289 @@
|
||||
import { preguntarTipoPresupuesto } from './presupuesto-utils.js';
|
||||
|
||||
(() => {
|
||||
// si jQuery está cargado, añade CSRF a AJAX
|
||||
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
|
||||
const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.getAttribute('content');
|
||||
if (window.$ && csrfToken && csrfHeader) {
|
||||
$.ajaxSetup({
|
||||
beforeSend: function (xhr) {
|
||||
xhr.setRequestHeader(csrfHeader, csrfToken);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const language = document.documentElement.lang || 'es-ES';
|
||||
|
||||
// Comprueba dependencias antes de iniciar
|
||||
if (!window.DataTable) {
|
||||
console.error('DataTables no está cargado aún');
|
||||
return;
|
||||
}
|
||||
|
||||
const table_anonimos = new DataTable('#presupuestos-anonimos-datatable', {
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
orderCellsTop: true,
|
||||
pageLength: 50,
|
||||
lengthMenu: [10, 25, 50, 100, 500],
|
||||
language: { url: '/assets/libs/datatables/i18n/' + language + '.json' },
|
||||
responsive: true,
|
||||
dom: 'lBrtip',
|
||||
buttons: {
|
||||
dom: {
|
||||
button: {
|
||||
className: 'btn btn-sm btn-outline-primary me-1'
|
||||
},
|
||||
buttons: [
|
||||
{ extend: 'copy' },
|
||||
{ extend: 'csv' },
|
||||
{ extend: 'excel' },
|
||||
{ extend: 'pdf' },
|
||||
{ extend: 'print' },
|
||||
{ extend: 'colvis' }
|
||||
],
|
||||
}
|
||||
},
|
||||
ajax: {
|
||||
url: '/presupuesto/datatable/anonimos',
|
||||
method: 'GET',
|
||||
},
|
||||
order: [[0, 'asc']],
|
||||
columns: [
|
||||
{ data: 'id', name: 'id', orderable: true },
|
||||
{ data: 'titulo', name: 'titulo', orderable: true },
|
||||
{ data: 'tipoEncuadernacion', name: 'tipoEncuadernacion', orderable: true },
|
||||
{ data: 'tipoCubierta', name: 'tipoCubierta', orderable: true },
|
||||
{ data: 'tipoImpresion', name: 'tipoImpresion', orderable: true },
|
||||
{ data: 'selectedTirada', name: 'selectedTirada', orderable: true },
|
||||
{ data: 'paginas', name: 'paginas', orderable: true },
|
||||
{ data: 'estado', name: 'estado', orderable: true },
|
||||
{ data: 'totalConIva', name: 'totalConIva', orderable: true },
|
||||
{ data: 'pais', name: 'pais', orderable: true },
|
||||
{ data: 'region', name: 'region', orderable: true },
|
||||
{ data: 'ciudad', name: 'ciudad', orderable: true },
|
||||
{ data: 'updatedAt', name: 'updatedAt', orderable: true },
|
||||
{ data: 'actions', orderable: false, searchable: false }
|
||||
],
|
||||
});
|
||||
|
||||
$('#presupuestos-anonimos-datatable').on('click', '.btn-edit-anonimo', function (e) {
|
||||
|
||||
e.preventDefault();
|
||||
const id = $(this).data('id');
|
||||
if (id) {
|
||||
window.location.href = '/presupuesto/view/' + id;
|
||||
}
|
||||
});
|
||||
|
||||
$('#presupuestos-anonimos-datatable').on('click', '.btn-delete-anonimo', function (e) {
|
||||
|
||||
e.preventDefault();
|
||||
const id = $(this).data('id');
|
||||
|
||||
Swal.fire({
|
||||
title: window.languageBundle.get(['presupuesto.delete.title']) || 'Eliminar presupuesto',
|
||||
html: window.languageBundle.get(['presupuesto.delete.text']) || 'Esta acción no se puede deshacer.',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-danger w-xs mt-2',
|
||||
cancelButton: 'btn btn-light w-xs mt-2'
|
||||
},
|
||||
confirmButtonText: window.languageBundle.get(['presupuesto.delete.button']) || 'Eliminar',
|
||||
cancelButtonText: window.languageBundle.get(['app.cancelar']) || 'Cancelar',
|
||||
}).then((result) => {
|
||||
if (!result.isConfirmed) return;
|
||||
|
||||
$.ajax({
|
||||
url: '/presupuesto/' + id,
|
||||
type: 'DELETE',
|
||||
success: function () {
|
||||
Swal.fire({
|
||||
icon: 'success', title: window.languageBundle.get(['presupuesto.delete.ok.title']) || 'Eliminado',
|
||||
text: window.languageBundle.get(['presupuesto.delete.ok.text']) || 'El presupuesto ha sido eliminado con éxito.',
|
||||
showConfirmButton: true,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary w-xs mt-2',
|
||||
},
|
||||
});
|
||||
$('#presupuestos-anonimos-datatable').DataTable().ajax.reload(null, false);
|
||||
},
|
||||
error: function (xhr) {
|
||||
// usa el mensaje del backend; fallback genérico por si no llega JSON
|
||||
const msg = (xhr.responseJSON && xhr.responseJSON.message)
|
||||
|| 'Error al eliminar el presupuesto.';
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'No se pudo eliminar',
|
||||
text: msg,
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
|
||||
cancelButton: 'btn btn-light' // clases para cancelar
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$('#presupuestos-anonimos-datatable').on('keyup', '.presupuesto-filter', function (e) {
|
||||
const colName = $(this).data('col');
|
||||
const colIndex = table_anonimos.column(colName + ':name').index();
|
||||
|
||||
if (table_anonimos.column(colIndex).search() !== this.value) {
|
||||
table_anonimos.column(colIndex).search(this.value).draw();
|
||||
}
|
||||
});
|
||||
|
||||
$('#presupuestos-anonimos-datatable').on('change', '.presupuesto-select-filter', function (e) {
|
||||
const colName = $(this).data('col');
|
||||
const colIndex = table_anonimos.column(colName + ':name').index();
|
||||
const value = $(this).val();
|
||||
table_anonimos.column(colIndex).search(value).draw();
|
||||
});
|
||||
|
||||
|
||||
const table_clientes = new DataTable('#presupuestos-clientes-datatable', {
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
orderCellsTop: true,
|
||||
pageLength: 50,
|
||||
lengthMenu: [10, 25, 50, 100, 500],
|
||||
language: { url: '/assets/libs/datatables/i18n/' + language + '.json' },
|
||||
responsive: true,
|
||||
dom: 'lBrtip',
|
||||
buttons: {
|
||||
dom: {
|
||||
button: {
|
||||
className: 'btn btn-sm btn-outline-primary me-1'
|
||||
},
|
||||
buttons: [
|
||||
{ extend: 'copy' },
|
||||
{ extend: 'csv' },
|
||||
{ extend: 'excel' },
|
||||
{ extend: 'pdf' },
|
||||
{ extend: 'print' },
|
||||
{ extend: 'colvis' }
|
||||
],
|
||||
}
|
||||
},
|
||||
ajax: {
|
||||
url: '/presupuesto/datatable/clientes',
|
||||
method: 'GET',
|
||||
},
|
||||
order: [[0, 'asc']],
|
||||
columns: [
|
||||
{ data: 'id', name: 'id', orderable: true },
|
||||
{ data: 'user', name: 'user.fullName', orderable: true },
|
||||
{ data: 'titulo', name: 'titulo', orderable: true },
|
||||
{ data: 'tipoEncuadernacion', name: 'tipoEncuadernacion', orderable: true },
|
||||
{ data: 'tipoCubierta', name: 'tipoCubierta', orderable: true },
|
||||
{ data: 'tipoImpresion', name: 'tipoImpresion', orderable: true },
|
||||
{ data: 'selectedTirada', name: 'selectedTirada', orderable: true },
|
||||
{ data: 'paginas', name: 'paginas', orderable: true },
|
||||
{ data: 'estado', name: 'estado', orderable: true },
|
||||
{ data: 'totalConIva', name: 'totalConIva', orderable: true },
|
||||
{ data: 'updatedAt', name: 'updatedAt', orderable: true },
|
||||
{ data: 'actions', orderable: false, searchable: false }
|
||||
],
|
||||
});
|
||||
|
||||
$('#presupuestos-clientes-datatable').on('click', '.btn-edit-privado', function (e) {
|
||||
|
||||
e.preventDefault();
|
||||
const id = $(this).data('id');
|
||||
if (id) {
|
||||
window.location.href = '/presupuesto/edit/' + id;
|
||||
}
|
||||
});
|
||||
|
||||
$('#presupuestos-clientes-datatable').on('click', '.btn-delete-privado', function (e) {
|
||||
|
||||
e.preventDefault();
|
||||
const id = $(this).data('id');
|
||||
|
||||
Swal.fire({
|
||||
title: window.languageBundle.get(['presupuesto.delete.title']) || 'Eliminar presupuesto',
|
||||
html: window.languageBundle.get(['presupuesto.delete.text']) || 'Esta acción no se puede deshacer.',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-danger w-xs mt-2',
|
||||
cancelButton: 'btn btn-light w-xs mt-2'
|
||||
},
|
||||
confirmButtonText: window.languageBundle.get(['presupuesto.delete.button']) || 'Eliminar',
|
||||
cancelButtonText: window.languageBundle.get(['app.cancelar']) || 'Cancelar',
|
||||
}).then((result) => {
|
||||
if (!result.isConfirmed) return;
|
||||
|
||||
$.ajax({
|
||||
url: '/presupuesto/' + id,
|
||||
type: 'DELETE',
|
||||
success: function () {
|
||||
Swal.fire({
|
||||
icon: 'success', title: window.languageBundle.get(['presupuesto.delete.ok.title']) || 'Eliminado',
|
||||
text: window.languageBundle.get(['presupuesto.delete.ok.text']) || 'El presupuesto ha sido eliminado con éxito.',
|
||||
showConfirmButton: true,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary w-xs mt-2',
|
||||
},
|
||||
});
|
||||
$('#presupuestos-clientes-datatable').DataTable().ajax.reload(null, false);
|
||||
},
|
||||
error: function (xhr) {
|
||||
// usa el mensaje del backend; fallback genérico por si no llega JSON
|
||||
const msg = (xhr.responseJSON && xhr.responseJSON.message)
|
||||
|| 'Error al eliminar el presupuesto.';
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'No se pudo eliminar',
|
||||
text: msg,
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
|
||||
cancelButton: 'btn btn-light' // clases para cancelar
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
$('#presupuestos-clientes-datatable').on('keyup', '.presupuesto-filter', function (e) {
|
||||
const colName = $(this).data('col');
|
||||
const colIndex = table_clientes.column(colName + ':name').index();
|
||||
|
||||
if (table_clientes.column(colIndex).search() !== this.value) {
|
||||
table_clientes.column(colIndex).search(this.value).draw();
|
||||
}
|
||||
});
|
||||
|
||||
$('#presupuestos-clientes-datatable').on('change', '.presupuesto-select-filter', function (e) {
|
||||
const colName = $(this).data('col');
|
||||
const colIndex = table_clientes.column(colName + ':name').index();
|
||||
const value = $(this).val();
|
||||
table_clientes.column(colIndex).search(value).draw();
|
||||
});
|
||||
|
||||
|
||||
$('#addPresupuestoButton').on('click', async function (e) {
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const res = await preguntarTipoPresupuesto();
|
||||
if (!res) return;
|
||||
|
||||
if (res.tipo === 'anonimo') {
|
||||
window.location.href = '/presupuesto/add/public';
|
||||
} else {
|
||||
window.location.href = '/presupuesto/add/private/' + res.clienteId;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
})();
|
||||
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Pregunta el tipo de presupuesto y (si aplica) selecciona cliente.
|
||||
* Devuelve una promesa con { tipo: 'anonimo'|'cliente', clienteId?: string }
|
||||
*/
|
||||
export async function preguntarTipoPresupuesto() {
|
||||
const { value: tipo } = await Swal.fire({
|
||||
title: window.languageBundle.get(['presupuesto.add.tipo']) || 'Selecciona tipo de presupuesto',
|
||||
input: 'radio',
|
||||
inputOptions: {
|
||||
anonimo: window.languageBundle.get(['presupuesto.add.anonimo']) || 'Anónimo',
|
||||
cliente: window.languageBundle.get(['presupuesto.add.cliente']) || 'De cliente'
|
||||
},
|
||||
inputValidator: (value) => {
|
||||
if (!value) {
|
||||
return window.languageBundle.get(['presupuesto.add.error.options']) || 'Debes seleccionar una opción.';
|
||||
}
|
||||
},
|
||||
confirmButtonText: window.languageBundle.get(['presupuesto.add.next']) || 'Siguiente',
|
||||
showCancelButton: true,
|
||||
cancelButtonText: window.languageBundle.get(['presupuesto.add.cancel']) || 'Cancelar',
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
|
||||
cancelButton: 'btn btn-light' // clases para cancelar
|
||||
},
|
||||
});
|
||||
|
||||
if (!tipo) return null; // Cancelado
|
||||
|
||||
if (tipo === 'anonimo') {
|
||||
return { tipo };
|
||||
}
|
||||
|
||||
// Si es de cliente, mostrar otro paso con Select2
|
||||
return Swal.fire({
|
||||
title: window.languageBundle.get(['presupuesto.add.select-client']) || 'Selecciona cliente',
|
||||
html: `
|
||||
<select id="selectCliente" class="form-select select2" style="width:100%"></select>
|
||||
`,
|
||||
focusConfirm: false,
|
||||
buttonsStyling: false,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: window.languageBundle.get(['presupuesto.add.next']) || 'Aceptar',
|
||||
cancelButtonText: window.languageBundle.get(['presupuesto.add.cancel']) || 'Cancelar',
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
|
||||
cancelButton: 'btn btn-light' // clases para cancelar
|
||||
},
|
||||
didOpen: () => {
|
||||
const $select = $('#selectCliente');
|
||||
// Configura Select2 (AJAX o lista estática)
|
||||
$select.select2({
|
||||
dropdownParent: $('.swal2-container'),
|
||||
ajax: {
|
||||
url: 'users/api/get-users', // ajusta a tu endpoint
|
||||
dataType: 'json',
|
||||
delay: 250,
|
||||
data: (params) => ({ q: params.term }),
|
||||
processResults: data => ({
|
||||
results: data.results,
|
||||
pagination: data.pagination
|
||||
}),
|
||||
cache: true
|
||||
}
|
||||
});
|
||||
},
|
||||
preConfirm: () => {
|
||||
const clienteId = $('#selectCliente').val();
|
||||
const clienteText = $('#selectCliente option:selected').text();
|
||||
if (!clienteId) {
|
||||
Swal.showValidationMessage(window.languageBundle.get(['presupuesto.add.error.select-client']) || 'Debes seleccionar un cliente.');
|
||||
return false;
|
||||
}
|
||||
return { tipo, clienteId, clienteText };
|
||||
}
|
||||
}).then((r) => (r.isConfirmed ? r.value : null));
|
||||
}
|
||||
@ -19,6 +19,22 @@ $(() => {
|
||||
pageLength: 50,
|
||||
language: { url: '/assets/libs/datatables/i18n/' + language + '.json' },
|
||||
responsive: true,
|
||||
dom: 'Bfrtip',
|
||||
buttons: {
|
||||
dom: {
|
||||
button: {
|
||||
className: 'btn btn-sm btn-outline-primary me-1'
|
||||
},
|
||||
buttons: [
|
||||
{ extend: 'copy' },
|
||||
{ extend: 'csv' },
|
||||
{ extend: 'excel' },
|
||||
{ extend: 'pdf' },
|
||||
{ extend: 'print' },
|
||||
{ extend: 'colvis' }
|
||||
],
|
||||
}
|
||||
},
|
||||
ajax: {
|
||||
url: '/users/datatable',
|
||||
method: 'GET',
|
||||
@ -116,7 +132,16 @@ $(() => {
|
||||
// usa el mensaje del backend; fallback genérico por si no llega JSON
|
||||
const msg = (xhr.responseJSON && xhr.responseJSON.message)
|
||||
|| 'Error al eliminar el usuario.';
|
||||
Swal.fire({ icon: 'error', title: 'No se pudo eliminar', text: msg });
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'No se pudo eliminar',
|
||||
text: msg,
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
|
||||
cancelButton: 'btn btn-light' // clases para cancelar
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
5
src/main/resources/static/assets/libs/datatables/buttons.colVis.min.js
vendored
Normal file
5
src/main/resources/static/assets/libs/datatables/buttons.colVis.min.js
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
/*!
|
||||
* Column visibility buttons for Buttons and DataTables.
|
||||
* © SpryMedia Ltd - datatables.net/license
|
||||
*/
|
||||
!function(i){var o,e;"function"==typeof define&&define.amd?define(["jquery","datatables.net","datatables.net-buttons"],function(n){return i(n,window,document)}):"object"==typeof exports?(o=require("jquery"),e=function(n,t){t.fn.dataTable||require("datatables.net")(n,t),t.fn.dataTable.Buttons||require("datatables.net-buttons")(n,t)},"undefined"==typeof window?module.exports=function(n,t){return n=n||window,t=t||o(n),e(n,t),i(t,0,n.document)}:(e(window,o),module.exports=i(o,window,window.document))):i(jQuery,window,document)}(function(n,t,i){"use strict";var e=n.fn.dataTable;return n.extend(e.ext.buttons,{colvis:function(n,t){var i=null,o={extend:"collection",init:function(n,t){i=t},text:function(n){return n.i18n("buttons.colvis","Column visibility")},className:"buttons-colvis",closeButton:!1,buttons:[{extend:"columnsToggle",columns:t.columns,columnText:t.columnText}]};return n.on("column-reorder.dt"+t.namespace,function(){n.button(null,n.button(null,i).node()).collectionRebuild([{extend:"columnsToggle",columns:t.columns,columnText:t.columnText}])}),o},columnsToggle:function(n,t){return n.columns(t.columns).indexes().map(function(n){return{extend:"columnToggle",columns:n,columnText:t.columnText}}).toArray()},columnToggle:function(n,t){return{extend:"columnVisibility",columns:t.columns,columnText:t.columnText}},columnsVisibility:function(n,t){return n.columns(t.columns).indexes().map(function(n){return{extend:"columnVisibility",columns:n,visibility:t.visibility,columnText:t.columnText}}).toArray()},columnVisibility:{columns:void 0,text:function(n,t,i){return i._columnText(n,i)},className:"buttons-columnVisibility",action:function(n,t,i,o){var t=t.columns(o.columns),e=t.visible();t.visible(void 0!==o.visibility?o.visibility:!(e.length&&e[0]))},init:function(e,n,t){var u=this,l=e.column(t.columns);n.attr("data-cv-idx",t.columns),e.on("column-visibility.dt"+t.namespace,function(n,t,i,o){l.index()!==i||t.bDestroying||t.nTable!=e.settings()[0].nTable||u.active(o)}).on("column-reorder.dt"+t.namespace,function(){t.destroying||1===e.columns(t.columns).count()&&(l=e.column(t.columns),u.text(t._columnText(e,t)),u.active(l.visible()))}),this.active(l.visible())},destroy:function(n,t,i){n.off("column-visibility.dt"+i.namespace).off("column-reorder.dt"+i.namespace)},_columnText:function(n,t){var i,o;return"string"==typeof t.text?t.text:(o=n.column(t.columns).title(),i=n.column(t.columns).index(),o=o.replace(/\n/g," ").replace(/<br\s*\/?>/gi," ").replace(/<select(.*?)<\/select\s*>/gi,""),o=e.Buttons.stripHtmlComments(o),o=e.util.stripHtml(o).trim(),t.columnText?t.columnText(n,i,o):o)}},colvisRestore:{className:"buttons-colvisRestore",text:function(n){return n.i18n("buttons.colvisRestore","Restore visibility")},init:function(n,t,i){n.columns().every(function(){var n=this.init();void 0===n.__visOriginal&&(n.__visOriginal=this.visible())})},action:function(n,t,i,o){t.columns().every(function(n){var t=this.init();this.visible(t.__visOriginal)})}},colvisGroup:{className:"buttons-colvisGroup",action:function(n,t,i,o){t.columns(o.show).visible(!0,!1),t.columns(o.hide).visible(!1,!1),t.columns.adjust()},show:[],hide:[]}}),e});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user