Compare commits

...

3 Commits

66 changed files with 2744 additions and 392 deletions

25
docker-compose.yml Normal file
View File

@ -0,0 +1,25 @@
version: '3.8'
services:
imprimelibros-db:
image: mysql:8.0
container_name: imprimelibros-db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: NrXz6DK6UoN
MYSQL_DATABASE: imprimelibros
MYSQL_USER: imprimelibros_user
MYSQL_PASSWORD: om91irrDctd
ports:
- "3306:3306"
volumes:
- db_data:/var/lib/mysql
networks:
- imprimelibros-network
volumes:
db_data:
networks:
imprimelibros-network:
driver: bridge

17
pom.xml
View File

@ -39,6 +39,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>nz.net.ultraq.thymeleaf</groupId>
<artifactId>thymeleaf-layout-dialect</artifactId>
@ -47,18 +51,25 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!--
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
@ -68,7 +79,7 @@
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>

View File

@ -0,0 +1,85 @@
package com.imprimelibros.erp.config.Presupuestador;
import java.util.Map;
public class ImagenPresupuesto {
private String id;
private String imagen;
private String alt;
private String texto;
private boolean selected;
private Map<String, String> extra_data;
// Constructores
public ImagenPresupuesto() {}
public ImagenPresupuesto(String id, String imagen, String alt, String texto, boolean selected) {
this.id = id;
this.imagen = imagen;
this.alt = alt;
this.texto = texto;
this.selected = selected;
}
public ImagenPresupuesto(String id, String imagen, String alt, String texto, Map<String, String> extra_data,boolean selected) {
this.id = id;
this.imagen = imagen;
this.alt = alt;
this.texto = texto;
this.selected = selected;
this.extra_data = extra_data;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getImagen() {
return imagen;
}
public void setImagen(String imagen) {
this.imagen = imagen;
}
public String getAlt() {
return alt;
}
public void setAlt(String alt) {
this.alt = alt;
}
public String getTexto() {
return texto;
}
public void setTexto(String texto) {
this.texto = texto;
}
public boolean isSelected() {
return selected;
}
public void setSelected(boolean selected) {
this.selected = selected;
}
public Map<String, String> getExtra_data() {
return extra_data;
}
public void setExtra_data(Map<String, String> extra_data) {
this.extra_data = extra_data;
}
}

View File

@ -0,0 +1,121 @@
package com.imprimelibros.erp.config.Presupuestador;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Component;
import java.util.Locale;
import java.util.Map;
@Component
public class PresupuestadorItems {
@Autowired
private MessageSource messageSource;
public ImagenPresupuesto getImpresionNegro(Locale locale) {
return new ImagenPresupuesto(
"negro",
"/assets/images/imprimelibros/presupuestador/blancoYnegro.png",
"",
messageSource.getMessage("presupuesto.blanco-negro", null, locale),
false);
}
public ImagenPresupuesto getImpresionNegroPremium(Locale locale) {
return new ImagenPresupuesto(
"negrohq",
"/assets/images/imprimelibros/presupuestador/negroFoto.png",
"",
messageSource.getMessage("presupuesto.blanco-negro-premium", null, locale),
false);
}
public ImagenPresupuesto getImpresionColor(Locale locale) {
String clave = "presupuesto.color"; // ✅
String texto = messageSource.getMessage(clave, null, locale);
System.out.println("Clave: " + clave + " => Texto: " + texto);
return new ImagenPresupuesto(
"color",
"/assets/images/imprimelibros/presupuestador/color.png",
"",
messageSource.getMessage("presupuesto.color", null, locale),
false);
}
public ImagenPresupuesto getImpresionColorPremium(Locale locale) {
return new ImagenPresupuesto(
"colorhq",
"/assets/images/imprimelibros/presupuestador/colorFoto.png",
"",
messageSource.getMessage("presupuesto.color-premium", null, locale),
false);
}
public ImagenPresupuesto getPapelOffsetBlanco(Locale locale) {
return new ImagenPresupuesto(
"offset-blanco",
"/assets/images/imprimelibros/presupuestador/offset-blanco.png",
"",
messageSource.getMessage("presupuesto.offset-blanco", null, locale),
Map.of("sk-id", "3"),
false);
}
public ImagenPresupuesto getPapelOffsetAhuesado(Locale locale) {
return new ImagenPresupuesto(
"offset-ahuesado",
"/assets/images/imprimelibros/presupuestador/offset-ahuesado.png",
"",
messageSource.getMessage("presupuesto.offset-ahuesado", null, locale),
Map.of("sk-id", "4"),
false);
}
public ImagenPresupuesto getPapelOffsetAhuesadoVolumen(Locale locale) {
return new ImagenPresupuesto(
"offset-ahuesado-volumen",
"/assets/images/imprimelibros/presupuestador/offset-ahuesado-volumen.png",
"",
messageSource.getMessage("presupuesto.offset-ahuesado-volumen", null, locale),
Map.of("sk-id", "6"),
false);
}
public ImagenPresupuesto getPapelEstucadoMate(Locale locale) {
return new ImagenPresupuesto(
"estucado-mate",
"/assets/images/imprimelibros/presupuestador/estucado-mate.png",
"",
messageSource.getMessage("presupuesto.estucado-mate", null, locale),
Map.of("sk-id", "2"),
false);
}
public ImagenPresupuesto getCartulinaGraficaCubierta(Locale locale) {
return new ImagenPresupuesto(
"cartulina-grafica-cubierta",
"/assets/images/imprimelibros/presupuestador/cartulina-grafica.png",
"",
messageSource.getMessage("presupuesto.cartulina-grafica-cubierta", null, locale),
Map.of("sk-id", "3"),
false);
}
public ImagenPresupuesto getEstucadoMateCubierta(Locale locale) {
return new ImagenPresupuesto(
"estucado-mate-cubierta",
"/assets/images/imprimelibros/presupuestador/estucado-mate-cubierta.png",
"",
messageSource.getMessage("presupuesto.estucado-mate-cubierta", null, locale),
Map.of("sk-id", "2"),
false);
}
}

View File

@ -4,6 +4,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
public class SecurityConfig {
@ -11,15 +12,24 @@ public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/assets/**", "/css/**", "/js/**", "/images/**", "/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(login -> login
//.loginPage("/login") añadir cuando se tenga login personalizado
.permitAll()
)
.logout(logout -> logout.permitAll());
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/",
"/assets/**",
"/css/**",
"/js/**",
"/images/**",
"/public/**",
"/error",
"/presupuesto/public/**")
.permitAll()
.anyRequest().authenticated())
.csrf(csrf -> csrf
.ignoringRequestMatchers("/presupuesto/public/**"))
.formLogin(login -> login
// .loginPage("/login") añadir cuando se tenga login personalizado
.permitAll())
.logout(logout -> logout.permitAll());
return http.build();
}

View File

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

View File

@ -0,0 +1,52 @@
package com.imprimelibros.erp.config.validation;
import com.imprimelibros.erp.entity.Presupuesto;
import com.imprimelibros.erp.service.VariableService;
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 ConsistentTiradasValidator implements ConstraintValidator<ConsistentTiradas, Presupuesto> {
@Autowired
private VariableService variableService;
@Autowired
private MessageSource messageSource;
@Override
public boolean isValid(Presupuesto presupuesto, ConstraintValidatorContext context) {
if (presupuesto == null)
return true;
Integer[] tiradas = presupuesto.getTiradas();
Integer podValue = variableService.getValorEntero("POD");
boolean allAbove = true;
boolean allBelow = true;
for (Integer t : tiradas) {
if (t == null)
continue;
if (t <= podValue)
allAbove = false;
else // (t > podValue)
allBelow = false;
}
if (!(allAbove || allBelow)) {
String mensajeInterpolado = messageSource.getMessage(
"presupuesto.errores.tiradas.consistentes", // clave del mensaje
new Object[] { podValue }, // parámetros para {0}
LocaleContextHolder.getLocale() // respeta el idioma actual
);
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(mensajeInterpolado)
.addConstraintViolation();
return false;
}
return true;
}
}

View File

@ -0,0 +1,16 @@
package com.imprimelibros.erp.config.validation;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
@Documented
@Constraint(validatedBy = ParValidator.class)
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
public @interface Par {
String message() default "El valor debe ser un número par";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@ -0,0 +1,14 @@
package com.imprimelibros.erp.config.validation;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
public class ParValidator implements ConstraintValidator<Par, Integer> {
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
if (value == null) return true; // se permite null, usa @NotNull aparte si lo necesitas
return value % 2 == 0;
}
}

View File

@ -0,0 +1,9 @@
package com.imprimelibros.erp.config.validation;
public class PresupuestoValidationGroups {
public interface DatosGenerales {}
public interface Interior {}
public interface Cubierta {}
}

View File

@ -0,0 +1,19 @@
package com.imprimelibros.erp.config.validation;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
@Documented
@Constraint(validatedBy = TamanioValidator.class)
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface Tamanio {
String message() default "{presupuesto.errores.tamanio.invalido}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@ -0,0 +1,54 @@
package com.imprimelibros.erp.config.validation;
import com.imprimelibros.erp.entity.Presupuesto;
import com.imprimelibros.erp.service.VariableService;
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 TamanioValidator implements ConstraintValidator<Tamanio, Presupuesto>{
@Autowired
private VariableService variableService;
@Autowired
private MessageSource messageSource;
@Override
public boolean isValid(Presupuesto presupuesto, ConstraintValidatorContext context) {
if (presupuesto == null)
return true;
Integer min = variableService.getValorEntero("ancho_alto_min");
Integer max = variableService.getValorEntero("ancho_alto_max");
if (presupuesto.getAncho() <= min || presupuesto.getAncho() >= max) {
String mensajeInterpolado = messageSource.getMessage(
"presupuesto.errores.ancho.min_max", // clave del mensaje
new Object[] { min, max }, // parámetros para {0}
LocaleContextHolder.getLocale() // respeta el idioma actual
);
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(mensajeInterpolado)
.addConstraintViolation();
return false;
}
else if (presupuesto.getAlto() <= min || presupuesto.getAlto() >= max) {
String mensajeInterpolado = messageSource.getMessage(
"presupuesto.errores.alto.min_max", // clave del mensaje
new Object[] { min, max }, // parámetros para {0}
LocaleContextHolder.getLocale() // respeta el idioma actual
);
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(mensajeInterpolado)
.addConstraintViolation();
return false;
}
return true;
}
}

View File

@ -1,24 +1,48 @@
package com.imprimelibros.erp.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import com.imprimelibros.erp.i18n.TranslationService;
import com.imprimelibros.erp.service.VariableService;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@Controller
public class HomeController {
@Autowired
private MessageSource messageSource;
private TranslationService translationService;
@Autowired
private VariableService variableService;
@GetMapping("/")
public String index(Model model, Locale locale) {
//model.addAttribute("title", messageSource.getMessage("t-home", null, locale));
model.addAttribute("title", "Inicio");
return "imprimelibros/home";
public String index(Model model, Authentication authentication, Locale locale) {
boolean isAuthenticated = authentication != null && authentication.isAuthenticated()
&& !(authentication instanceof AnonymousAuthenticationToken);
if (!isAuthenticated) {
List<String> keys = List.of(
"presupuesto.plantilla-cubierta",
"presupuesto.plantilla-cubierta-text",
"presupuesto.impresion-cubierta",
"presupuesto.impresion-cubierta-help");
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"));
}
return "imprimelibros/home";
}
}

View File

@ -0,0 +1,108 @@
package com.imprimelibros.erp.controller;
import org.springframework.web.bind.annotation.RestController;
import com.imprimelibros.erp.service.PresupuestoService;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Collections;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.PostMapping;
import com.imprimelibros.erp.config.validation.PresupuestoValidationGroups;
import com.imprimelibros.erp.entity.Presupuesto;
@RestController
@RequestMapping("/presupuesto")
public class PresupuestoController {
@Autowired
protected PresupuestoService presupuestoService;
@PostMapping("/public/validar/datos-generales")
public ResponseEntity<?> validarDatosGenerales(
@Validated(PresupuestoValidationGroups.DatosGenerales.class) Presupuesto presupuesto,
BindingResult result, Locale locale) {
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()));
if (!errores.isEmpty()) {
return ResponseEntity.badRequest().body(errores);
}
// opciones color
Map<String, Object> resultado = presupuestoService.obtenerOpcionesColor(presupuesto, locale);
// opciones papel interior
resultado.putAll(presupuestoService.obtenerOpcionesPapelInterior(presupuesto, locale));
// opciones gramaje interior
resultado.putAll(presupuestoService.obtenerOpcionesGramajeInterior(presupuesto));
return ResponseEntity.ok(resultado);
}
@PostMapping("/public/validar/interior")
public ResponseEntity<?> validarInterior(
@Validated(PresupuestoValidationGroups.Interior.class) Presupuesto presupuesto,
BindingResult result, Locale locale) {
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()));
if (!errores.isEmpty()) {
return ResponseEntity.badRequest().body(errores);
}
return ResponseEntity.ok(Collections.singletonMap("success", true));
}
@PostMapping("/public/get-gramaje-interior")
public ResponseEntity<?> getGramajeInterior(
@Validated(PresupuestoValidationGroups.Interior.class) Presupuesto presupuesto,
BindingResult result) {
Map<String, String> errores = new HashMap<>();
// errores de campos individuales
result.getFieldErrors().forEach(error -> errores.put(error.getField(), error.getDefaultMessage()));
if (!errores.isEmpty()) {
return ResponseEntity.badRequest().body(errores);
}
Map<String, Object> resultado = presupuestoService.obtenerOpcionesGramajeInterior(presupuesto);
return ResponseEntity.ok(resultado);
}
@PostMapping("/public/get-papel-cubierta")
public ResponseEntity<?> getPapelCubierta(
Presupuesto presupuesto,
BindingResult result, Locale locale) {
Map<String, Object> resultado = presupuestoService.obtenerOpcionesPapelCubierta(presupuesto, locale);
return ResponseEntity.ok(resultado);
}
}

View File

@ -0,0 +1,375 @@
package com.imprimelibros.erp.entity;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.persistence.*;
import com.imprimelibros.erp.config.validation.PresupuestoValidationGroups;
import com.imprimelibros.erp.config.validation.Tamanio;
import com.imprimelibros.erp.config.validation.ConsistentTiradas;
import com.imprimelibros.erp.config.validation.Par;
@ConsistentTiradas(groups = PresupuestoValidationGroups.DatosGenerales.class)
@Tamanio(groups = PresupuestoValidationGroups.DatosGenerales.class)
@Entity
@Table(name = "presupuesto")
public class Presupuesto {
public enum TipoEncuadernacion {
fresado, cosido, grapado, espiral, wireo
}
public enum TipoImpresion {
negro, negrohq, color, colorhq
}
public enum TipoCubierta{
tapaBlanda, tapaDuraLomoRecto, tapaDuraLomoRedondo
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull(message = "{presupuesto.errores.tipo-encuadernacion}", groups = PresupuestoValidationGroups.DatosGenerales.class)
@Column(name = "tipo_encuadernacion")
private TipoEncuadernacion tipoEncuadernacion = TipoEncuadernacion.fresado;
@NotBlank(message = "{presupuesto.errores.titulo}", groups = PresupuestoValidationGroups.DatosGenerales.class)
@Column(name = "titulo")
private String titulo;
@Column(name = "autor")
private String autor;
@Column(name = "isbn")
private String isbn;
@NotNull(message = "{presupuesto.errores.tirada1}", groups = PresupuestoValidationGroups.DatosGenerales.class)
@Column(name = "tirada1")
private Integer tirada1;
@Column(name = "tirada2")
private Integer tirada2;
@Column(name = "tirada3")
private Integer tirada3;
@Column(name = "tirada4")
private Integer tirada4;
@NotNull(message = "{presupuesto.errores.ancho}", groups = PresupuestoValidationGroups.DatosGenerales.class)
@Column(name = "ancho")
private Integer ancho;
@NotNull(message = "{presupuesto.errores.alto}", groups = PresupuestoValidationGroups.DatosGenerales.class)
@Column(name = "alto")
private Integer alto;
@Column(name = "formatoPersonalizado")
private Boolean formatoPersonalizado;
@Par(message = "{presupuesto.errores.paginasNegro.par}", groups = PresupuestoValidationGroups.DatosGenerales.class)
@NotNull(message = "{presupuesto.errores.paginasNegro.required}", groups = PresupuestoValidationGroups.DatosGenerales.class)
@Column(name = "paginas_negro")
private Integer paginasNegro;
@Par(message = "{presupuesto.errores.paginasColor.par}", groups = PresupuestoValidationGroups.DatosGenerales.class)
@NotNull(message = "{presupuesto.errores.paginasColor.required}", groups = PresupuestoValidationGroups.DatosGenerales.class)
@Column(name = "paginas_color")
private Integer paginasColor;
@Column(name = "posicion_paginas_color")
private String posicionPaginasColor;
@Column(name = "paginas_color_total")
private Integer paginasColorTotal;
@NotNull(message = "{presupuesto.errores.tipo-impresion}", groups = PresupuestoValidationGroups.Interior.class)
@Column(name = "tipo_impresion")
private TipoImpresion tipoImpresion = TipoImpresion.negro;
@NotNull(message = "{presupuesto.errores.papel-interior}", groups = PresupuestoValidationGroups.Interior.class)
@Column(name = "papel_interior_id")
private Integer papelInteriorId;
@NotNull(message = "{presupuesto.errores.gramaje-interior}", groups = PresupuestoValidationGroups.Interior.class)
@Column(name = "gramaje_interior")
private Integer gramejeInterior;
@NotNull(message = "{presupuesto.errores.tipo-cubierta}", groups = PresupuestoValidationGroups.Cubierta.class)
@Column(name = "tipo_cubierta")
private TipoCubierta tipoCubierta = TipoCubierta.tapaBlanda;
@Column(name = "solapas_cubierta")
private Boolean solapasCubierta = false;
@Column(name = "tamanio_solapas_cubierta")
private Integer tamanioSolapasCubierta;
@Column(name = "cubierta_caras")
private Integer cubiertaCaras;
@Column(name = "papel_guardas_id")
private Integer papelGuardasId;
@Column(name = "gramaje_guardas")
private Integer gramajeGuardas;
@Column(name = "guardas_impresas")
private Boolean guardasImpresas;
@Column(name = "cabezada")
private String cabezada;
@NotNull(message = "{presupuesto.errores.papel-cubierta}", groups = PresupuestoValidationGroups.Cubierta.class)
@Column(name = "papel_cubierta_id")
private Integer papelCubiertaId = 2;
@NotNull(message = "{presupuesto.errores.gramaje-cubierta}", groups = PresupuestoValidationGroups.Cubierta.class)
@Column(name = "gramaje_cubierta")
private Integer gramajeCubierta = 240;
// Getters y Setters
public String getAutor() {
return autor;
}
public void setAutor(String autor) {
this.autor = autor;
}
public String getIsbn() {
return isbn;
}
public void setIsbn(String isbn) {
this.isbn = isbn;
}
public Integer getTirada1() {
return tirada1;
}
public void setTirada1(Integer tirada1) {
this.tirada1 = tirada1;
}
public Integer getTirada2() {
return tirada2;
}
public void setTirada2(Integer tirada2) {
this.tirada2 = tirada2;
}
public Integer getTirada3() {
return tirada3;
}
public void setTirada3(Integer tirada3) {
this.tirada3 = tirada3;
}
public Integer getTirada4() {
return tirada4;
}
public Integer[] getTiradas() {
return new Integer[]{tirada1, tirada2, tirada3, tirada4};
}
public void setTirada4(Integer tirada4) {
this.tirada4 = tirada4;
}
public Integer getAncho() {
return ancho;
}
public void setAncho(Integer ancho) {
this.ancho = ancho;
}
public Integer getAlto() {
return alto;
}
public void setAlto(Integer alto) {
this.alto = alto;
}
public Boolean getFormatoPersonalizado() {
return formatoPersonalizado;
}
public void setFormatoPersonalizado(Boolean formatoPersonalizado) {
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;
}
public void setPaginasNegro(Integer paginasNegro) {
this.paginasNegro = paginasNegro;
}
public Integer getPaginasColor() {
return paginasColor;
}
public void setPaginasColor(Integer paginasColor) {
this.paginasColor = paginasColor;
}
public TipoEncuadernacion getTipoEncuadernacion() {
return tipoEncuadernacion;
}
public void setTipoEncuadernacion(TipoEncuadernacion tipoEncuadernacion) {
this.tipoEncuadernacion = tipoEncuadernacion;
}
public String getPosicionPaginasColor() {
return posicionPaginasColor;
}
public void setPosicionPaginasColor(String posicionPaginasColor) {
this.posicionPaginasColor = posicionPaginasColor;
}
public Integer getPaginasColorTotal() {
return paginasColorTotal;
}
public void setPaginasColorTotal(Integer paginasColorTotal) {
this.paginasColorTotal = paginasColorTotal;
}
public TipoImpresion getTipoImpresion() {
return tipoImpresion;
}
public void setTipoImpresion(TipoImpresion tipoImpresion) {
this.tipoImpresion = tipoImpresion;
}
public Integer getPapelInteriorId() {
return papelInteriorId;
}
public void setPapelInteriorId(Integer papelInteriorId) {
this.papelInteriorId = papelInteriorId;
}
public Integer getGramejeInterior() {
return gramejeInterior;
}
public void setGramejeInterior(Integer gramejeInterior) {
this.gramejeInterior = gramejeInterior;
}
public TipoCubierta getTipoCubierta() {
return tipoCubierta;
}
public void setTipoCubierta(TipoCubierta tipoCubierta) {
this.tipoCubierta = tipoCubierta;
}
public Boolean getSolapasCubierta() {
return solapasCubierta;
}
public void setSolapasCubierta(Boolean solapasCubierta) {
this.solapasCubierta = solapasCubierta;
}
public Integer getTamanioSolapasCubierta() {
return tamanioSolapasCubierta;
}
public void setTamanioSolapasCubierta(Integer tamanioSolapasCubierta) {
this.tamanioSolapasCubierta = tamanioSolapasCubierta;
}
public Integer getCubiertaCaras() {
return cubiertaCaras;
}
public void setCubiertaCaras(Integer cubiertaCaras) {
this.cubiertaCaras = cubiertaCaras;
}
public Integer getPapelGuardasId() {
return papelGuardasId;
}
public void setPapelGuardasId(Integer papelGuardasId) {
this.papelGuardasId = papelGuardasId;
}
public Integer getGramajeGuardas() {
return gramajeGuardas;
}
public void setGramajeGuardas(Integer gramajeGuardas) {
this.gramajeGuardas = gramajeGuardas;
}
public Boolean getGuardasImpresas() {
return guardasImpresas;
}
public void setGuardasImpresas(Boolean guardasImpresas) {
this.guardasImpresas = guardasImpresas;
}
public String getCabezada() {
return cabezada;
}
public void setCabezada(String cabezada) {
this.cabezada = cabezada;
}
public Integer getPapelCubiertaId() {
return papelCubiertaId;
}
public void setPapelCubiertaId(Integer papelCubiertaId) {
this.papelCubiertaId = papelCubiertaId;
}
public Integer getGramajeCubierta() {
return gramajeCubierta;
}
public void setGramajeCubierta(Integer gramajeCubierta) {
this.gramajeCubierta = gramajeCubierta;
}
}

View File

@ -0,0 +1,30 @@
package com.imprimelibros.erp.entity;
import jakarta.persistence.*;
@Entity
@Table(name = "variables")
public class Variable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String clave;
private String valor;
// Getters y setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getClave() { return clave; }
public void setClave(String clave) { this.clave = clave; }
public String getValor() { return valor; }
public void setValor(String valor) { this.valor = valor; }
}

View File

@ -0,0 +1,22 @@
package com.imprimelibros.erp.i18n;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Component;
import java.util.*;
@Component
public class TranslationService {
@Autowired
private MessageSource messageSource;
public Map<String, String> getTranslations(Locale locale, List<String> keys) {
Map<String, String> translations = new HashMap<>();
for (String key : keys) {
translations.put(key, messageSource.getMessage(key, null, locale));
}
return translations;
}
}

View File

@ -0,0 +1,10 @@
package com.imprimelibros.erp.repository;
import com.imprimelibros.erp.entity.Variable;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface VariableRepository extends JpaRepository<Variable, Long> {
Optional<Variable> findByClave(String clave);
}

View File

@ -0,0 +1,210 @@
package com.imprimelibros.erp.service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Locale;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.imprimelibros.erp.config.Presupuestador.ImagenPresupuesto;
import com.imprimelibros.erp.config.Presupuestador.PresupuestadorItems;
import com.imprimelibros.erp.entity.Presupuesto;
@Service
public class PresupuestoService {
@Autowired
protected VariableService variableService;
private final PresupuestadorItems presupuestadorItems;
public PresupuestoService(PresupuestadorItems presupuestadorItems) {
this.presupuestadorItems = presupuestadorItems;
}
public boolean validateDatosGenerales(int[] tiradas) {
for (int tirada : tiradas) {
if (tirada <= 0) {
return false; // Invalid tirada found
}
}
return true;
}
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) ||
(presupuesto.getTirada3() != null && presupuesto.getTirada3() <= pod_value) ||
(presupuesto.getTirada4() != null && presupuesto.getTirada4() <= pod_value);
}
public Map<String, Object> obtenerOpcionesColor(Presupuesto presupuesto, Locale locale) {
List<ImagenPresupuesto> opciones = new ArrayList<>();
ImagenPresupuesto opcion;
if (presupuesto.getPaginasColor() > 0) {
if (!this.isPOD(presupuesto)) {
// POD solo color foto
opcion = this.presupuestadorItems.getImpresionColor(locale);
opcion.setSelected(presupuesto.getTipoImpresion() == Presupuesto.TipoImpresion.color);
opciones.add(opcion);
}
opcion = this.presupuestadorItems.getImpresionColorPremium(locale);
opcion.setSelected(presupuesto.getTipoImpresion() == Presupuesto.TipoImpresion.colorhq);
opciones.add(opcion);
} else {
opcion = this.presupuestadorItems.getImpresionNegro(locale);
opcion.setSelected(presupuesto.getTipoImpresion() == Presupuesto.TipoImpresion.negro);
opciones.add(opcion);
opcion = this.presupuestadorItems.getImpresionNegroPremium(locale);
opcion.setSelected(presupuesto.getTipoImpresion() == Presupuesto.TipoImpresion.negrohq);
opciones.add(opcion);
}
boolean opcionSeleccionada = opciones.stream()
.findFirst()
.map(op -> {
op.setSelected(true);
return true;
})
.orElse(false);
if (!opcionSeleccionada) {
opciones.get(0).setSelected(true);
presupuesto.setPapelInteriorId(Integer.parseInt(opciones.get(0).getExtra_data().get("sk-id")));
}
Map<String, Object> response = new HashMap<>();
response.put("opciones_color", opciones);
return response;
}
public Map<String, Object> obtenerOpcionesPapelInterior(Presupuesto presupuesto, Locale locale) {
List<ImagenPresupuesto> opciones = new ArrayList<>();
opciones.add(this.presupuestadorItems.getPapelOffsetBlanco(locale));
opciones.add(this.presupuestadorItems.getPapelOffsetAhuesado(locale));
opciones.add(this.presupuestadorItems.getPapelOffsetAhuesadoVolumen(locale));
opciones.add(this.presupuestadorItems.getPapelEstucadoMate(locale));
for (ImagenPresupuesto imagenPresupuesto : opciones) {
imagenPresupuesto.setSelected(
presupuesto.getPapelInteriorId() != null
&& imagenPresupuesto.getExtra_data().get("sk-id").equals(
String.valueOf(presupuesto.getPapelInteriorId())));
}
boolean opcionSeleccionada = opciones.stream()
.findFirst()
.map(opcion -> {
opcion.setSelected(true);
return true;
})
.orElse(false);
if (!opcionSeleccionada) {
opciones.get(0).setSelected(true);
presupuesto.setPapelInteriorId(Integer.parseInt(opciones.get(0).getExtra_data().get("sk-id")));
}
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;
final int AHUESADO_OFFSET_ID = 4;
final int AHUESADO_OFFSET_VOLUMEN_ID = 6;
final int ESTUCADO_MATE_ID = 2;
if (presupuesto.getPapelInteriorId() != null && presupuesto.getPapelInteriorId() == BLANCO_OFFSET_ID) {
if (presupuesto.getTipoImpresion() == Presupuesto.TipoImpresion.negro ||
presupuesto.getTipoImpresion() == Presupuesto.TipoImpresion.color) {
gramajes.add("80");
}
gramajes.add("90");
} else if (presupuesto.getPapelInteriorId() != null && presupuesto.getPapelInteriorId() == AHUESADO_OFFSET_ID) {
if (presupuesto.getTipoImpresion() == Presupuesto.TipoImpresion.negro ||
presupuesto.getTipoImpresion() == Presupuesto.TipoImpresion.color) {
gramajes.add("80");
}
gramajes.add("150");
gramajes.add("170");
} 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("90");
gramajes.add("100");
gramajes.add("150");
gramajes.add("170");
} else if (presupuesto.getPapelInteriorId() != null && presupuesto.getPapelInteriorId() == ESTUCADO_MATE_ID) {
gramajes.add("90");
gramajes.add("100");
gramajes.add("115");
if (presupuesto.getTipoImpresion() == Presupuesto.TipoImpresion.color) {
gramajes.add("120");
}
gramajes.add("135");
}
Map<String, Object> response = new HashMap<>();
response.put("opciones_gramaje_interior", gramajes);
return response;
}
public Map<String, Object> obtenerOpcionesPapelCubierta(Presupuesto presupuesto, Locale locale) {
List<ImagenPresupuesto> opciones = new ArrayList<>();
if (presupuesto.getTipoCubierta() == Presupuesto.TipoCubierta.tapaBlanda) {
opciones.add(this.presupuestadorItems.getCartulinaGraficaCubierta(locale));
}
opciones.add(this.presupuestadorItems.getEstucadoMateCubierta(locale));
for (ImagenPresupuesto imagenPresupuesto : opciones) {
imagenPresupuesto.setSelected(
presupuesto.getPapelCubiertaId() != null
&& imagenPresupuesto.getExtra_data().get("sk-id").equals(
String.valueOf(presupuesto.getPapelCubiertaId())));
}
boolean opcionSeleccionada = opciones.stream()
.findFirst()
.map(opcion -> {
opcion.setSelected(true);
return true;
})
.orElse(false);
if (!opcionSeleccionada) {
opciones.get(0).setSelected(true);
presupuesto.setPapelInteriorId(Integer.parseInt(opciones.get(0).getExtra_data().get("sk-id")));
}
Map<String, Object> response = new HashMap<>();
response.put("opciones_papel_cubierta", opciones);
return response;
}
}

View File

@ -0,0 +1,20 @@
package com.imprimelibros.erp.service;
import com.imprimelibros.erp.repository.VariableRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class VariableService {
@Autowired
private VariableRepository variableRepository;
public Integer getValorEntero(String clave) {
return variableRepository.findByClave(clave)
.<Integer>map(v -> Integer.parseInt(v.getValor()))
.orElseThrow(
() -> new IllegalArgumentException("No se encontró la variable con clave '" + clave + "'"));
}
}

View File

@ -1,2 +1,13 @@
spring.application.name=erp
logging.level.org.springframework.security=DEBUG
logging.level.root=WARN
logging.level.org.springframework=ERROR
spring.datasource.url=jdbc:mysql://localhost:3306/imprimelibros
spring.datasource.username=imprimelibros_user
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

View File

@ -1,7 +1,7 @@
presupuesto.datos-generales=Datos Generales
presupuesto.interior=Interior
presupuesto.cubierta=Cubierta
presupuesto.envio=Envío
presupuesto.extras=Extras
# Pestaña datos generales de presupuesto
presupuesto.informacion-libro=Información del libro
@ -13,18 +13,98 @@ presupuesto.tirada1=Tirada 1*
presupuesto.tirada2=Tirada 2
presupuesto.tirada3=Tirada 3
presupuesto.tirada4=Tirada 4
presupuesto.tiradasPODnoPOD=No puede mezclar tiradas menores de 30 unidades con mayores de 30 unidades
presupuesto.tiradasPODnoPOD=No puede mezclar tiradas menores de {0} unidades con mayores de {0} unidades
presupuesto.formato=Formato
presupuesto.orientacion.vertical=Vertical
presupuesto.orientacion.apaisado=Apaisado
presupuesto.orientacion.cuadrado=Cuadrado
presupuesto.formato-personalizado=Formato personalizado
presupuesto.ancho=Ancho
presupuesto.alto=Alto
presupuesto.ancho=Ancho (mm)*
presupuesto.alto=Alto (mm)*
presupuesto.paginas-total=Total Páginas
presupuesto.paginas-negro=Páginas Negro
presupuesto.paginas-color=Páginas Color
presupuesto.paginas-posicion=Posición páginas color
presupuesto.paginas-posicion-descripcion=Intruzca la posición separada por comas. Ej: 3,5,7 ó 4-10,20,155
presupuesto.siempre-pares=Siempre deben ser pares
presupuesto.encuadernacion=Encuadernación
presupuesto.fresado-descripcion=Fresado (a partir de 32 páginas)
presupuesto.cosido-descripcion=Cosido (a partir de 32 páginas)
presupuesto.grapado-descripcion=Grapado (entre 12 y 40 páginas)
presupuesto.espiral-descripcion=Espiral (a partir de 20 páginas)
presupuesto.wire-o-descripcion=Wire-O (a partir de 20 páginas)
presupuesto.encuadernacion-descripcion=Seleccione la encuadernación del libro
presupuesto.siguiente=Siguiente
presupuesto.continuar-interior=Continuar a diseño interior
# Pestaña interior de presupuesto
presupuesto.tipo-encuadernacion=Tipo de impresión
presupuesto.tipo-encuadernacion-descripcion=Seleccione entre calidad estándar o premium
presupuesto.papel-interior=Papel interior
presupuesto.papel-interior-descripcion=Seleccione el papel para el interior
presupuesto.gramaje-interior=Gramaje interior
presupuesto.gramaje-interior-descripcion=Seleccione el gramaje para el interior
presupuesto.blanco-negro=Blanco y negro
presupuesto.blanco-negro-premium= Blanco y negro Premium
presupuesto.color=Color
presupuesto.color-premium=Color Premium
presupuesto.offset-blanco=Offset Blanco
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
# Pestaña cubierta
presupuesto.plantilla-cubierta=Plantilla de cubierta
presupuesto.plantilla-cubierta-text=Recuerde que la cubierta es el conjunto formado por la portada, contraportada, lomo y solapas, en caso de que las lleve.<br/><br/><br/>Si tiene dudas de las medidas puede solicitarnos una plantilla cuando haga el pedido.
presupuesto.tipo-cubierta=Tipo de cubierta
presupuesto.tipo-cubierta-descripcion=Seleccione el tipo de cubierta y sus opciones
presupuesto.tapa-blanda=Tapa blanda
presupuesto.tapa-dura=Tapa dura
presupuesto.tapa-dura-lomo-redondo=Tapa dura lomo redondo
presupuesto.sin-solapas=Sin solapas
presupuesto.con-solapas=Con solapas
presupuesto.impresion-cubierta=Impresión de cubierta
presupuesto.impresion-cubierta-help=La cubierta se puede imprimir por anverso y reverso, como en el caso de las revistas, pero para un libro normal con portada y contraportada, la impresión de cubierta es a una cara.
presupuesto.una-cara=Una cara
presupuesto.dos-caras=Dos caras
presupuesto.tamanio-solapa=Tamaño solapa
presupuesto.papel-guardas=Papel de guardas
presupuesto.guardas-impresas=Guardas impresas
presupuesto.no=No
presupuesto.cabezada=Cabezada
presupuesto.cabezada-blanca=Blanca
presupuesto.cabezada-verde=Verde
presupuesto.cabezada-azul=Azul
presupuesto.cabezada-roja-amarilla=Roja-Amarilla
presupuesto.papel-cubierta=Papel cubierta
presupuesto.papel-cubierta-descripcion=Seleccione el papel para la cubierta
presupuesto.cartulina-grafica-cubierta=Cartulina gráfica estucada a una cara
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-extras-libro=Continuar a extras del libro
# Errores
presupuesto.errores-title=Corrija los siguientes errores:
presupuesto.errores.titulo=El título es obligatorio
presupuesto.errores.tirada1=La tirada 1 es obligatoria
presupuesto.errores.tiradas.consistentes=Las tiradas deben ser consistentes (no mezclar tiradas menores de {0} unidades con mayores de {0} unidades)
presupuesto.errores.paginasNegro.required=El número de páginas en negro es obligatorio
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-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;
presupuesto.errores.alto=El alto no puede estar vacío
presupuesto.errores.alto.min_max=El alto tiene que estar en el rango [{0}, {1}] mm
presupuesto.errores.tipo-impresion=Seleccione el tipo de impresion
presupuesto.errores.papel-interior=Seleccione el tipo de papel para el interior
presupuesto.errores.gramaje-interior=Seleccione el gramaje del papel para el interior
presupuesto.errores.tipo-cubierta=Seleccione el tipo de cubierta
presupuesto.errores.solapas-cubierta=Seleccione si desea o no solapas en la cubierta
presupuesto.errores.papel-cubierta=Seleccione el tipo de papel para la cubierta
presupuesto.errores.gramaje-cubierta=Seleccione el gramaje del papel para la cubierta

View File

@ -7,8 +7,98 @@ Website: https://themesbrand.com/
Contact: support@themesbrand.com
File: Main Css File
*/
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Public+Sans:wght@300;400;500;600;700&display=swap");
/* poppins-300 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Poppins';
font-style: normal;
font-weight: 300;
src: url('../fonts/poppins/poppins-v23-latin-300.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* poppins-regular - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Poppins';
font-style: normal;
font-weight: 400;
src: url('../fonts/poppins/poppins-v23-latin-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* poppins-500 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Poppins';
font-style: normal;
font-weight: 500;
src: url('../fonts/poppins/poppins-v23-latin-500.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* poppins-600 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Poppins';
font-style: normal;
font-weight: 600;
src: url('../fonts/poppins/poppins-v23-latin-600.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* poppins-700 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Poppins';
font-style: normal;
font-weight: 700;
src: url('../fonts/poppins/poppins-v23-latin-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* public-sans-300 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Public Sans';
font-style: normal;
font-weight: 300;
src: url('../fonts/public-sans/public-sans-v20-latin-300.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* public-sans-regular - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Public Sans';
font-style: normal;
font-weight: 400;
src: url('../fonts/public-sans/public-sans-v20-latin-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* public-sans-500 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Public Sans';
font-style: normal;
font-weight: 500;
src: url('../fonts/public-sans/public-sans-v20-latin-500.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* public-sans-600 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Public Sans';
font-style: normal;
font-weight: 600;
src: url('../fonts/public-sans/public-sans-v20-latin-600.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* public-sans-700 - latin */
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Public Sans';
font-style: normal;
font-weight: 700;
src: url('../fonts/public-sans/public-sans-v20-latin-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
@font-face {
font-family: "hkgrotesk";
src: url("../fonts/hkgrotesk-light.eot");

File diff suppressed because one or more lines are too long

View File

@ -18,6 +18,8 @@
border-radius: 8px;
padding: 8px;
margin-inline: 5px;
transition: border 0.3s ease;
}
@ -58,3 +60,36 @@
max-width: 100%;
}
}
.gramaje-radio{
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 70px; /* Ancho mínimo */
min-height: 70px; /* Alto mínimo */
max-width: 70px; /* Ancho máximo */
max-height: 70px; /* Alto máximo */
}
@keyframes fadeInUpBounce {
0% {
opacity: 0;
transform: translateY(30px);
}
60% {
opacity: 1;
transform: translateY(-10px);
}
80% {
transform: translateY(5px);
}
100% {
transform: translateY(0);
}
}
.animate-fadeInUpBounce {
animation: fadeInUpBounce 0.6s ease-out both;
animation-delay: 0.1s;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 KiB

View File

@ -0,0 +1,7 @@
window.languageBundle.get = function (key, ...params) {
let text = this[key] || key;
params.forEach((val, i) => {
text = text.replace(`{${i}}`, val);
});
return text;
};

View File

@ -1,23 +1,65 @@
$(() => {
$('.imagen-container-group').on('click', '.imagen-selector', function () {
const clicked = $(this);
const group = clicked.closest('.imagen-container-group');
class imagen_presupuesto {
// Limpiar selección anterior
group.find('.imagen-selector').removeClass('selected preselected')
.find('.image-presupuesto').removeClass('selected');
constructor(data) {
this.id = data.id;
this.imagen = data.imagen;
this.alt = data.alt || "";
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.extraData = data.extra_data || {}; // Datos extra opcionales
}
// Marcar nuevo seleccionado
clicked.addClass('selected');
const img = clicked.find('.image-presupuesto');
img.addClass('selected');
render() {
const contenedor = $('<div>', {
id: this.id,
class: `${this.extraClass + ' '}image-container imagen-selector${this.selected ? ' selected' : ''}`
});
// Añadir atributos extra al contenedor
for (const [key, value] of Object.entries(this.extraData)) {
contenedor.attr(`data-${key}`, value);
}
// Aplicar animación (reset antes para que se repita)
img.removeClass('zoom-anim');
void img[0].offsetWidth; // "reflow" para reiniciar animación
img.addClass('zoom-anim');
const input = $('<input>', {
type: 'radio',
name: this.group,
value: this.id,
hidden: true
});
// Guardar ID en hidden si lo necesitas
$('#tipoEncuadernacionSeleccionada').val(clicked.attr('id'));
});
});
const imagen = $('<img>', {
class: 'image-presupuesto',
id: this.id + '-img',
src: this.imagen,
alt: this.alt
});
const etiqueta = $('<label>', {
for: this.id + '-img',
class: 'form-label',
text: this.texto
});
contenedor.append(imagen, etiqueta);
return contenedor;
}
setSelected(selected) {
this.selected = selected;
const contenedor = $(`#${this.id}`);
const radio = contenedor.find('input[type="radio"]');
if (selected) {
contenedor.addClass('selected');
radio.prop('checked', true).trigger('change'); // <-- esto actualiza el input y lanza evento
} else {
contenedor.removeClass('selected');
radio.prop('checked', false);
}
}
}
export default imagen_presupuesto;

View File

@ -0,0 +1,20 @@
$('.imagen-container-group').on('click', '.image-container', function () {
const clicked = $(this);
const group = clicked.closest('.imagen-container-group');
// Limpiar selección anterior
group.find('.image-container').removeClass('selected')
.find('.image-presupuesto').removeClass('zoom-anim');
// Marcar nuevo seleccionado
clicked.addClass('selected');
// Aplicar animación de zoom
const img = clicked.find('.image-presupuesto');
void img[0].offsetWidth; // Forzar reflow
img.addClass('zoom-anim');
clicked.find('input[type="radio"]').prop('checked', true).trigger('change');
});

View File

@ -0,0 +1,692 @@
import imagen_presupuesto from "./imagen-presupuesto.js";
class PresupuestoCliente {
constructor() {
this.DEBUG = true; // Activar o desactivar el modo de depuración
this.formData = {
datosGenerales: {
titulo: '',
autor: '',
isbn: '',
tirada1: '',
tirada2: '',
tirada3: '',
tirada4: '',
ancho: '',
alto: '',
formatoPersonalizado: false,
paginasNegro: '',
paginasColor: '',
posicionPaginasColor: '',
tipoEncuadernacion: 'fresado',
},
interior: {
tipoImpresion: 'negro',
papelInteriorId: 3,
gramajeInterior: 9,
},
cubierta: {
tipoCubierta: 'tapaBlanda',
solapasCubierta: 0,
tamanioSolapasCubierta: '80',
cubiertaCaras: 2,
papelGuardasId: 3,
gramajeGuardas: 170,
guardasImpresas: 0,
cabezada: 'WHI',
papelCubiertaId: 3,
gramajeCubierta: 170,
}
}
// pestaña datos generales
this.divContentDatosGenerales = $('#content-datos-generales');
this.titulo = $('#titulo');
this.autor = $('#autor');
this.isbn = $('#isbn');
this.tirada1 = $('#tirada1');
this.tirada2 = $('#tirada2');
this.tirada3 = $('#tirada3');
this.tirada4 = $('#tirada4');
this.formatoPersonalizado = $('#formato-personalizado');
this.divFormato = $('.div-formato');
this.divFormatoPersonalizado = $('.div-formato-personalizado');
this.formato = $('#formato');
this.ancho = $('#ancho');
this.alto = $('#alto');
this.paginasNegro = $('#paginas-negro');
this.paginasColor = $('#paginas-color');
this.divPosicionPaginasColor = $('#div-posicion-paginas-color');
this.posicionPaginasColor = $('#posicionPaginasColor');
this.paginas = $('#paginas');
this.btn_next_datos_generales = $('#next-datos-generales');
this.datos_generales_alert = $('#datos-generales-alert');
// pestaña interior
this.divContentInterior = $('#content-interior');
this.divOpcionesColor = $('#div-opciones-color');
this.divPapelInterior = $('#div-papel-interior');
this.divGramajeInterior = $("#div-gramaje-interior");
this.interior_alert = $('#interior-alert');
// pestaña cubierta
this.divSolapasCubierta = $('#div-solapas-cubierta');
this.divPapelCubierta = $('#div-papel-cubierta');
this.divGramajeCubierta = $("#div-gramaje-cubierta");
this.btn_plantilla_cubierta = $('#btn-plantilla-cubierta');
this.btn_impresion_cubierta_help = $('#impresion-cubierta-help');
}
init() {
this.#initDatosGenerales();
this.#initInterior();
this.#initCubierta();
const stored = sessionStorage.getItem("formData");
if (stored) {
this.formData = JSON.parse(stored);
this.#loadDatosGeneralesData();
}
}
#cacheFormData() {
sessionStorage.setItem("formData", JSON.stringify(this.formData));
}
#changeTab(idContenidoTab) {
const tabButton = document.querySelector(`[data-bs-target="#${idContenidoTab}"]`);
if (tabButton) {
const tab = new bootstrap.Tab(tabButton);
tab.show();
}
}
#getPresupuestoData() {
return {
...this.#getDatosGeneralesData(),
...this.#getInteriorData(),
...this.#getCubiertaData()
};
}
#addGramaje(contenedor, gramaje, name) {
const id = `gramaje-${gramaje}`;
// Crear input
const input = document.createElement('input');
input.type = 'radio';
input.className = 'btn-check';
input.dataset.gramaje = gramaje;
input.id = id;
input.name = name;
// Crear label
const label = document.createElement('label');
label.className = 'btn btn-outline-primary material-shadow gramaje-radio';
label.setAttribute('for', id);
label.textContent = gramaje;
// Añadir al contenedor
contenedor.append(input);
contenedor.append(label);
}
/******************************
* END OF DATOS GENERALES
******************************/
#initDatosGenerales() {
$('.datos-generales-data').on('change', () => {
const dataToStore = this.#getDatosGeneralesData();
this.#updateDatosGeneralesData(dataToStore);
this.#cacheFormData();
});
$('.paginas').on('change', () => {
this.paginas.val(parseInt(this.paginasNegro.val()) + parseInt(this.paginasColor.val()));
if (parseInt(this.paginasColor.val()) == 0) {
this.divPosicionPaginasColor.addClass('d-none');
this.posicionPaginasColor.val("");
}
else {
this.divPosicionPaginasColor.removeClass('d-none');
}
this.#updateTipoEncuadernacion();
});
this.formatoPersonalizado.on('change', () => {
if (this.formatoPersonalizado.is(':checked')) {
this.divFormato.addClass('d-none');
this.divFormatoPersonalizado.removeClass('d-none');
}
else {
this.divFormatoPersonalizado.addClass('d-none');
this.divFormato.removeClass('d-none');
}
});
this.btn_next_datos_generales.on('click', () => {
this.#nextDatosGenerales();
});
}
#nextDatosGenerales() {
let data = this.#getPresupuestoData();
$.ajax({
url: '/presupuesto/public/validar/datos-generales',
type: 'POST',
data: data,
success: (data) => {
this.#processDatosGenerales(data);
},
error: (xhr, status, error) => {
this.datos_generales_alert.removeClass('d-none');
this.datos_generales_alert.find('#datos-generales-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.datos_generales_alert.find('#datos-generales-alert-list').append(`<li>${errorMsg}</li>`);
});
} else {
this.datos_generales_alert.find('#datos-generales-alert-list').append('<li>Error desconocido. Por favor, inténtelo de nuevo más tarde.</li>');
}
$(window).scrollTop(0);
}
});
}
#processDatosGenerales(data) {
this.datos_generales_alert.addClass('d-none');
this.#loadInteriorData(data);
const interiorData = this.#getInteriorData();
this.#updateInteriorData(interiorData);
this.#changeTab('pills-inside');
this.divContentInterior.addClass('animate-fadeInUpBounce');
}
#getDatosGeneralesData() {
const tamanio = this.#getTamanio();
return {
titulo: this.titulo.val(),
autor: this.autor.val(),
isbn: this.isbn.val(),
tirada1: this.tirada1.val(),
tirada2: this.tirada2.val(),
tirada3: this.tirada3.val(),
tirada4: this.tirada4.val(),
formatoPersonalizado: this.formatoPersonalizado.is(':checked'),
ancho: parseInt(tamanio.ancho),
alto: parseInt(tamanio.alto),
paginasNegro: this.paginasNegro.val(),
paginasColor: this.paginasColor.val(),
posicionPaginasColor: this.posicionPaginasColor.val(),
tipoEncuadernacion: ($('.tipo-libro.selected').length > 0) ? $('.tipo-libro.selected').attr('id') : 'fresado',
};
}
#updateDatosGeneralesData(data) {
this.formData.datosGenerales = {
titulo: data.titulo,
autor: data.autor,
isbn: data.isbn,
tirada1: data.tirada1,
tirada2: data.tirada2,
tirada3: data.tirada3,
tirada4: data.tirada4,
formatoPersonalizado: data.formatoPersonalizado,
ancho: parseInt(data.ancho),
alto: parseInt(data.alto),
paginasNegro: data.paginasNegro,
paginasColor: data.paginasColor,
posicionPaginasColor: data.posicionPaginasColor,
tipoEncuadernacion: data.tipoEncuadernacion,
};
}
#loadDatosGeneralesData() {
this.titulo.val(this.formData.datosGenerales.titulo);
this.autor.val(this.formData.datosGenerales.autor);
this.isbn.val(this.formData.datosGenerales.isbn);
this.tirada1.val(this.formData.datosGenerales.tirada1);
this.tirada2.val(this.formData.datosGenerales.tirada2);
this.tirada3.val(this.formData.datosGenerales.tirada3);
this.tirada4.val(this.formData.datosGenerales.tirada4);
this.paginasNegro.val(this.formData.datosGenerales.paginasNegro);
this.paginasColor.val(this.formData.datosGenerales.paginasColor);
this.posicionPaginasColor.val(this.formData.datosGenerales.posicionPaginasColor);
$('.tipo-libro').removeClass('selected');
$('.image-container#' + this.formData.datosGenerales.tipoEncuadernacion).trigger('click');
this.#updateTipoEncuadernacion();
this.formatoPersonalizado.prop('checked', this.formData.datosGenerales.formatoPersonalizado).trigger('change');
$('.paginas').trigger('change');
if (this.formatoPersonalizado.is(':checked')) {
this.ancho.val(this.formData.datosGenerales.ancho);
this.alto.val(this.formData.datosGenerales.alto);
} else {
const option = this.formato.find('option').filter(() => {
return $(this).data('ancho') == this.formData.datosGenerales.ancho &&
$(this).data('alto') == this.formData.datosGenerales.alto;
});
if (option.length) {
this.formato.val(option.val()).trigger('change');
}
}
}
#getTamanio() {
if (this.formatoPersonalizado.is(':checked')) {
return {
ancho: this.ancho.val(),
alto: this.alto.val()
};
}
else {
const opcionSeleccionada = $('#formato option:selected');
const ancho = opcionSeleccionada.data('ancho');
const alto = opcionSeleccionada.data('alto');
return {
ancho: ancho,
alto: alto
};
}
}
#updateTipoEncuadernacion() {
const paginas = parseInt(this.paginas.val());
const selectedTipo = $('.tipo-libro.selected').attr('id');
$('.tipo-libro').removeClass('selected');
if (paginas < 32) {
$('.tipo-libro#fresado').addClass('d-none');
$('.tipo-libro#cosido').addClass('d-none');
}
else {
$('.tipo-libro#fresado').removeClass('d-none');
$('.tipo-libro#cosido').removeClass('d-none');
}
if (paginas < 20) {
$('.tipo-libro#espiral').addClass('d-none');
$('.tipo-libro#wireo').addClass('d-none');
}
else {
$('.tipo-libro#espiral').removeClass('d-none');
$('.tipo-libro#wireo').removeClass('d-none');
}
if (paginas < 12 || paginas > 40) {
$('.tipo-libro#grapado').addClass('d-none');
}
else {
$('.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 {
let firstVisible = $('.tipo-libro').not('.d-none').first();
if (firstVisible.length) {
firstVisible.addClass('selected');
}
}
if ($('.tipo-libro.selected').length > 0) {
this.formData.datosGenerales.tipoEncuadernacion = $('.tipo-libro.selected').attr('id');
}
else {
this.formData.datosGenerales.tipoEncuadernacion = '';
}
}
/******************************
* END OF DATOS GENERALES
******************************/
/******************************
* INTERIOR
******************************/
#initInterior() {
$(document).on('click', '.interior-data', (e) => {
const dataToStore = this.#getInteriorData();
this.#updateInteriorData(dataToStore);
this.#cacheFormData();
});
$(document).on('click', '.papel-interior', (e) => {
this.divGramajeInterior.removeClass('animate-fadeInUpBounce');
const data = this.#getPresupuestoData();
$.ajax({
url: '/presupuesto/public/get-gramaje-interior',
type: 'POST',
data: data,
}).done((data) => {
this.divGramajeInterior.empty();
const gramajes = data.opciones_gramaje_interior;
this.#addGramajesInterior(gramajes);
this.divGramajeInterior.addClass('animate-fadeInUpBounce');
const dataInterior = this.#getInteriorData();
this.#updateInteriorData(dataInterior);
this.#cacheFormData();
}).fail((xhr, status, error) => {
});
});
$('.btn-change-tab-interior').on('click', (e) => {
let data = this.#getPresupuestoData();
const id = e.currentTarget.id;
$.ajax({
url: '/presupuesto/public/validar/interior',
type: 'POST',
data: data,
success: (data) => {
if( id === 'btn-prev-interior') {
this.#changeTab('pills-general-data');
} else {
$('.tapa-cubierta.selected').trigger('click');
this.#changeTab('pills-cover');
}
},
error: (xhr, status, error) => {
this.interior_alert.removeClass('d-none');
this.interior_alert.find('#inside-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>`);
});
} else {
this.interior_alert.find('#inside-alert-list').append('<li>Error desconocido. Por favor, inténtelo de nuevo más tarde.</li>');
}
$(window).scrollTop(0);
}
});
});
}
#loadInteriorData(data) {
this.divOpcionesColor.empty();
this.divPapelInterior.empty();
this.divGramajeInterior.empty();
const opciones_color = data.opciones_color;
for (let i = 0; i < opciones_color.length; i++) {
const opcion = opciones_color[i];
const item = new imagen_presupuesto(opcion);
item.extraClass = 'interior-data';
if ((this.formData.interior.color === '' && i === 0) ||
this.formData.interior.color === opcion.id) {
item.setSelected(true);
}
this.divOpcionesColor.append(item.render());
}
const opciones_papel_interior = data.opciones_papel_interior;
for (let i = 0; i < opciones_papel_interior.length; i++) {
const opcion = opciones_papel_interior[i];
const item = new imagen_presupuesto(opcion);
item.extraClass = 'interior-data papel-interior';
this.divPapelInterior.append(item.render());
}
const gramajes = data.opciones_gramaje_interior;
this.#addGramajesInterior(gramajes);
const dataInterior = this.#getInteriorData();
this.#updateInteriorData(dataInterior);
this.#cacheFormData();
}
#addGramajesInterior(gramajes) {
for (let i = 0; i < gramajes.length; i++) {
const gramaje = gramajes[i];
this.#addGramaje(this.divGramajeInterior, gramaje, 'gramaje-interior');
// Seleccionar el gramaje por defecto
if (this.formData.interior.gramaje === '' && i === 0) {
$(`#gramaje-${gramaje}`).prop('checked', true);
} else if (this.formData.interior.gramaje === gramaje) {
$(`#gramaje-${gramaje}`).prop('checked', true);
}
}
if (this.divGramajeInterior.find('input[type="radio"]:checked').length === 0) {
// If not, select the first one by default
this.divGramajeInterior.find('input[type="radio"]').first().prop('checked', true);
}
}
#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 gramejeInterior = $('input[name="gramaje-interior"]:checked').data('gramaje') || 90;
return {
tipoImpresion: tipoImpresion,
papelInteriorId: papelInteriorId,
gramejeInterior: gramejeInterior
};
}
#updateInteriorData(data) {
this.formData.interior.tipoImpresion = data.tipoImpresion;
this.formData.interior.papelInteriorId = data.papelInteriorId;
this.formData.interior.gramejeInterior = data.gramejeInterior;
}
/******************************
* END INTERIOR
******************************/
/******************************
* CUBIERTA
******************************/
#initCubierta() {
this.btn_plantilla_cubierta.on('click', () => {
Swal.fire({
position: 'top-end',
icon: 'info',
title: window.languageBundle.get('presupuesto.plantilla-cubierta'),
html: `
<div class="text-center p-4">
<img src="/assets/images/imprimelibros/presupuestador/plantilla-cubierta.png" class="img-fluid" alt="" />
</div>
<div class="acitivity-timeline p-4">
${window.languageBundle.get('presupuesto.plantilla-cubierta-text')}
</div>
`,
confirmButtonClass: 'btn btn-primary w-xs mt-2',
showConfirmButton: false,
showCloseButton: true
});
});
this.btn_impresion_cubierta_help.on('click', () => {
Swal.fire({
position: 'top-end',
icon: 'info',
title: window.languageBundle.get('presupuesto.impresion-cubierta'),
html: window.languageBundle.get('presupuesto.impresion-cubierta-help'),
confirmButtonClass: 'btn btn-primary w-xs mt-2',
showConfirmButton: false,
showCloseButton: true
});
});
$(document).on('click', '.tapa-cubierta', (e) => {
$('.tapa-dura-options').eq(0).removeClass('animate-fadeInUpBounce');
$('.tapa-blanda-options').eq(0).removeClass('animate-fadeInUpBounce');
if(e.currentTarget.id === 'tapaBlanda') {
$('.tapa-dura-options').addClass('d-none');
$('.tapa-blanda-options').removeClass('d-none');
$('.tapa-blanda-options').eq(0).addClass('animate-fadeInUpBounce');
}
else{
$('.tapa-blanda-options').addClass('d-none');
$('.tapa-dura-options').removeClass('d-none');
$('.tapa-dura-options').eq(0).addClass('animate-fadeInUpBounce');
}
this.#getPapelesCubierta(e.currentTarget.id);
const dataToStore = this.#getCubiertaData();
this.#updateCubiertaData(dataToStore);
this.#cacheFormData();
});
$(document).on('click', '.solapas-cubierta', (e) => {
if(e.currentTarget.id === 'sin-solapas') {
this.divSolapasCubierta.addClass('d-none');
}
else{
this.divSolapasCubierta.removeClass('d-none');
}
const dataToStore = this.#getCubiertaData();
this.#updateCubiertaData(dataToStore);
this.#cacheFormData();
});
}
#getPapelesCubierta(tapa_id) {
this.divPapelCubierta.removeClass('animate-fadeInUpBounce');
return $.ajax({
url: '/presupuesto/public/get-papel-cubierta',
type: 'POST',
data: {
tipoCubierta: tapa_id,
papelCubiertaId: this.formData.cubierta.papelCubiertaId,
gramajeCubierta: this.formData.cubierta.gramajeCubierta,
},
success: (data) => {
this.divPapelCubierta.empty();
this.divGramajeCubierta.empty();
const papelesCubierta = data.opciones_papel_cubierta;
for (let i = 0; i < papelesCubierta.length; i++) {
const papel = papelesCubierta[i];
const item = new imagen_presupuesto(papel);
item.extraClass = 'cubierta-data papel-cubierta';
if (papel.id === this.formData.cubierta.papelCubiertaId) {
item.setSelected(true);
}
this.divPapelCubierta.append(item.render());
}
this.divPapelCubierta.addClass('animate-fadeInUpBounce');
},
error: (xhr, status, error) => {
console.error("Error al obtener los papeles de cubierta: ", xhr.responseText);
$(window).scrollTop(0);
}
});
}
#getCubiertaData() {
const tipoCubierta = $('.cubierta-data.tapa.selected').attr('id') || 'tapaBlanda';
const solapas = parseInt($('#div-solapas-cubierta input[type="radio"]:checked').val()) || 0;
const tamanioSolapas = $('#tamanio-solapas-cubierta').val() || '80';
const cubiertaCaras = parseInt($('#cubierta-caras').val()) || 2;
const guardasPapel = parseInt($('#guardas-papel').val()) || 3;
const guardasGramaje = parseInt($('#guardas-gramaje').val()) || 170;
const guardasImpresas = parseInt($('#guardas-impresas').val()) || 0;
const cabezada = $('#cabezada').val() || 'WHI';
const papelCubiertaId = $('#papel-cubierta-id').val() || 3;
const gramajeCubierta = $('#gramaje-cubierta').val() || 170;
return {
tipoCubierta: tipoCubierta,
solapas: solapas,
tamanioSolapas: tamanioSolapas,
cubiertaCaras: cubiertaCaras,
guardasPapel: guardasPapel,
guardasGramaje: guardasGramaje,
guardasImpresas: guardasImpresas,
cabezada: cabezada,
papelCubiertaId: papelCubiertaId,
gramajeCubierta: gramajeCubierta
};
}
#updateCubiertaData(data) {
this.formData.cubierta.tipoCubierta = data.tipoCubierta;
this.formData.cubierta.solapas = data.solapas;
this.formData.cubierta.tamanioSolapas = data.tamanioSolapas;
this.formData.cubierta.cubiertaCaras = data.cubiertaCaras;
this.formData.cubierta.guardasPapel = data.guardasPapel;
this.formData.cubierta.guardasGramaje = data.guardasGramaje;
this.formData.cubierta.guardasImpresas = data.guardasImpresas;
this.formData.cubierta.cabezada = data.cabezada;
this.formData.cubierta.papelCubiertaId = data.papelCubiertaId;
this.formData.cubierta.gramajeCubierta = data.gramajeCubierta;
}
/******************************
* END CUBIERTA
******************************/
}
document.addEventListener('DOMContentLoaded', function () {
const presupuestoCliente = new PresupuestoCliente();
presupuestoCliente.init();
});

View File

@ -247,7 +247,6 @@ if (document.getElementById("sa-position"))
icon: 'success',
title: 'Your work has been saved',
showConfirmButton: false,
timer: 1500,
showCloseButton: true
})
});

View File

@ -2,7 +2,7 @@
const head = document.head || document.getElementsByTagName("head")[0];
const scripts = [
"https://cdn.jsdelivr.net/npm/toastify-js",
"/assets/libs/toastify-js/src/toastify.js",
"/assets/libs/choices.js/public/assets/scripts/choices.min.js",
"/assets/libs/flatpickr/flatpickr.min.js",
"/assets/libs/feather-icons/feather.min.js" // <- AÑADIMOS feather aquí

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -34,8 +34,12 @@
<th:block th:replace="~{theme/partials/vendor-scripts :: scripts}" />
<th:block layout:fragment="pagejs">
<div th:unless="${#authorization.expression('isAuthenticated()')}">
<script th:src="@{/assets/js/pages/imprimelibros/presupuestador/imagen-presupuesto.js}"></script>
<script th:src="@{/assets/js/pages/imprimelibros/presupuestador/imagen-selector.js}"></script>
<script type="module" th:src="@{/assets/js/pages/imprimelibros/presupuestador/presupuestador.js}"></script>
</div>
<script th:inline="javascript">
window.languageBundle = /*[[${languageBundle}]]*/ {};
</script>
</th:block>
</body>

View File

@ -1,18 +1,13 @@
<html th:lang="${#locale.language}"
th:with="isAuth=${#authorization.expression('isAuthenticated()')}"
th:attrappend="data-layout=${isAuth} ? 'semibox' : 'horizontal'"
data-sidebar-visibility="show"
data-topbar="light"
data-sidebar="light"
data-sidebar-size="lg"
data-sidebar-image="none"
data-preloader="disable"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<html th:lang="${#locale.language}" th:with="isAuth=${#authorization.expression('isAuthenticated()')}"
th:attrappend="data-layout=${isAuth} ? 'semibox' : 'horizontal'" data-sidebar-visibility="show" data-topbar="light"
data-sidebar="light" data-sidebar-size="lg" data-sidebar-image="none" data-preloader="disable"
xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<th:block layout:fragment="pagetitle" />
<th:block th:replace="~{imprimelibros/partials/head-css :: head-css}" />
<link href="/assets/libs/sweetalert2/sweetalert2.min.css" rel="stylesheet" type="text/css" />
<link href="/assets/libs/select2/select2.min.css" rel="stylesheet" />
<th:block layout:fragment="pagecss" />
</head>
@ -32,11 +27,13 @@
</section>
<th:block layout:fragment="modal" />
<!-- de momento comenta vendor-scripts si no lo usas -->
<th:block th:replace="~{theme/partials/vendor-scripts :: scripts}" />
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="/assets/libs/jquery/jquery-3.6.0.min.js"></script>
<script src="/assets/libs/sweetalert2/sweetalert2.min.js"></script>
<script src="/assets/libs/select2/select2.min.js"></script>
<th:block layout:fragment="pagejs" />
<script th:src="@{/assets/js/app.js}"></script>
<script th:src="@{/assets/js/pages/imprimelibros/languageBundle.js}"></script>
</body>
</html>

View File

@ -0,0 +1,204 @@
<div class="animate-fadeInUpBounce">
<div class="row justify-content-center mb-3">
<div class="col-auto">
<button type="button" id="btn-plantilla-cubierta" class="btn btn-outline-primary btn-border">
<i class="ri-questionnaire-line label-icon align-middle fs-16 me-2"></i>
<span th:text="#{presupuesto.plantilla-cubierta}">
Plantilla de cubierta
</span>
</button>
</div>
</div>
<!-- Ribbon Shape -->
<div class="card ribbon-box border shadow-none mb-lg-0 material-shadow">
<div class="card-body">
<div class="ribbon ribbon-primary ribbon-shape" th:text="#{presupuesto.tipo-cubierta}">Tipo cubierta
</div>
<h5 class="fs-14 text-end" th:text="#{presupuesto.tipo-cubierta-descripcion}">Seleccione el tipo de cubierta
</h5>
</div>
<div class="ribbon-content mt-4">
<div class="row justify-content-center imagen-container-group mt-3">
<div id="tapaBlanda" class="tapa-cubierta image-container imagen-selector selected">
<img class="image-presupuesto" src="/assets/images/imprimelibros/presupuestador/tapa-blanda.png"
alt="">
<label for="titulo" class="form-label" th:text="#{presupuesto.tapa-blanda}">
Tapa blanda
</label>
</div>
<div id="tapaDuraLomoRecto" class="tapa-cubierta image-container imagen-selector">
<img class="image-presupuesto"
src="/assets/images/imprimelibros/presupuestador/tapa-dura-lomo-recto.png" alt="">
<label class="form-label" th:text="#{presupuesto.tapa-dura}">
Tapa dura
</label>
</div>
<div id="tapaDuraLomoRedondo" class="tapa-cubierta image-container imagen-selector">
<img class="image-presupuesto"
src="/assets/images/imprimelibros/presupuestador/tapa-dura-lomo-redondo.png" alt="">
<label class="form-label" th:text="#{presupuesto.tapa-dura-lomo-redondo}">
Tapa dura lomo redondo
</label>
</div>
</div>
<div class="row justify-content-center align-items-center mt-3 imagen-container-group tapa-blanda-options">
<div id="sin-solapas" class="image-container imagen-selector solapas-cubierta selected">
<img class="image-presupuesto"
src="/assets/images/imprimelibros/presupuestador/sinSolapasCubierta.png" alt="">
<label class="form-label" th:text="#{presupuesto.sin-solapas}">
Sin solapas
</label>
</div>
<div id="con-solapas" class="image-container imagen-selector solapas-cubierta">
<img class="image-presupuesto"
src="/assets/images/imprimelibros/presupuestador/conSolapasCubierta.png" alt="">
<label class="form-label" th:text="#{presupuesto.con-solapas}">
Con solapas
</label>
</div>
<div class="col-auto mb-3">
<label for="impresion-cubierta" class="form-label"
th:text="#{presupuesto.impresion-cubierta}">Impresión de cubierta</label>
<div class="input-group">
<select class="form-select select2" id="impresion-cubierta">
<option value="2" th:text="#{presupuesto.una-cara}">Una cara</option>
<option value="4" th:text="#{presupuesto.dos-caras}">Dos cara</option>
</select>
<button class="btn btn-outline-primary btn-icon waves-effect waves-light material-shadow-none"
type="button" id="impresion-cubierta-help">
<i class="ri-questionnaire-line"></i>
</button>
</div>
</div>
<div id="div-solapas-cubierta" class="col-auto mb-3 d-none">
<label for="tamanio-solapas-cubierta" class="form-label"
th:text="#{presupuesto.tamanio-solapa}">Tamaño solapa</label>
<input type="number" class="form-control" id="tamanio-solapas-cubierta" value="80" step="1">
</div>
</div>
<div class="row justify-content-center align-items-center mt-3 imagen-container-group tapa-dura-options d-none">
<div class="col-auto mb-3">
<label for="papel-guardas" class="form-label" th:text="#{presupuesto.papel-guardas}">Papel de
guardas</label>
<select class="form-select select2" id="papel-guardas">
<option value="1" data-papel-id="3" data-gramaje="170"
th:text="#{presupuesto.offset-blanco} + ' 170 gr'" selected>Offset blanco 170 gr</option>
<option value="2" data-papel-id="4" data-gramaje="170"
th:text="#{presupuesto.offset-ahuesado} + ' 170 gr'">Offset ahuesado 170 gr</option>
<option value="3" data-papel-id="2" data-gramaje="170"
th:text="#{presupuesto.estucado-mate} + ' 170 gr'">Estucado mate 170 gr</option>
</select>
</div>
<div class="col-auto mb-3">
<label for="guardas-impresas" class="form-label" th:text="#{presupuesto.guardas-impresas}">Guardas
impresas</label>
<select class="form-select select2" id="guardas-impresas">
<option value="0" th:text="#{presupuesto.no}" selected>No</option>
<option value="4" th:text="#{presupuesto.una-cara}">Una cara</option>
<option value="8" th:text="#{presupuesto.dos-caras}">Dos caras</option>
</select>
</div>
<div class="col-auto mb-3">
<label for="cabezada" class="form-label" th:text="#{presupuesto.cabezada}">Cabezada</label>
<select class="form-select select2" id="guardas-impresas">
<option value="WHI" th:text="#{presupuesto.cabezada-blanca}" selected>Blanca</option>
<option value="GRE" th:text="#{presupuesto.cabezada-verde}">Verde</option>
<option value="BLUE" th:text="#{presupuesto.cabezada-azul}">Azul</option>
<option value="REDYEL" th:text="#{presupuesto.cabezada-roja-amarilla}">Roja-Amarilla</option>
</select>
</div>
</div>
</div>
</div>
<!-- End Ribbon Shape -->
<div class="card ribbon-box border shadow-none mb-lg-0 material-shadow mt-4">
<div class="card-body">
<div class="ribbon ribbon-primary ribbon-shape" th:text="#{presupuesto.papel-cubierta}">Papel cubierta</div>
<h5 class="fs-14 text-end" th:text="#{presupuesto.papel-cubierta-descripcion}">
Seleccione el papel para la cubierta</h5>
</div>
<div class="ribbon-content mt-4">
<div id="div-papel-cubierta" class="row justify-content-center imagen-container-group">a
</div>
</div>
</div>
<!-- End Ribbon Shape -->
<!-- Ribbon Shape -->
<div class="card ribbon-box border shadow-none mb-lg-0 material-shadow mt-4">
<div class="card-body">
<div class="ribbon ribbon-primary ribbon-shape" th:text="#{presupuesto.gramaje-cubierta}">Gramaje cubierta
</div>
<h5 class="fs-14 text-end" th:text="#{presupuesto.gramaje-cubierta-descripcion}">
Gramaje del interior</h5>
</div>
<div class="ribbon-content mt-4">
<div id="div-gramaje-cubierta" class="hstack gap-2 justify-content-center flex-wrap">
<input type="radio" class="btn-check" id="gramaje-cubierta-240" name="gramaje-cubierta"
data-gramaje="240">
<label class="btn btn-outline-primary material-shadow gramaje-radio" for="gramaje-cubierta-240">
240
</label>
<input type="radio" class="btn-check" id="gramaje-cubierta-250" name="gramaje-cubierta"
data-gramaje="250">
<label class="btn btn-outline-primary material-shadow gramaje-radio" for="gramaje-cubierta-250">
250
</label>
<input type="radio" class="btn-check" id="gramaje-cubierta-270" name="gramaje-cubierta"
data-gramaje="270">
<label class="btn btn-outline-primary material-shadow gramaje-radio" for="gramaje-cubierta-270">
270
</label>
<input type="radio" class="btn-check" id="gramaje-cubierta-300" name="gramaje-cubierta"
data-gramaje="300" checked>
<label class="btn btn-outline-primary material-shadow gramaje-radio" for="gramaje-cubierta-300">
<span class="invisible small">_</span>
300
<span class="text-warning small fw-bold"></span>
</label>
<input type="radio" class="btn-check" id="gramaje-cubierta-350" name="gramaje-cubierta"
data-gramaje="350">
<label class="btn btn-outline-primary material-shadow gramaje-radio" for="gramaje-cubierta-350">
350
</label>
</div>
</div>
</div>
<!-- End Ribbon Shape -->
<div class="d-flex justify-content-between align-items-center mt-4 w-100">
<button type="button" class="btn btn-light d-flex align-items-center">
<i class=" ri-arrow-left-circle-line label-icon align-middle fs-16 me-2"></i>
<span th:text="#{presupuesto.volver-interior}">Volver a interior</span>
</button>
<button type="button" class="btn btn-primary d-flex align-items-center">
<span th:text="#{presupuesto.continuar-extras-libro}">Continuar a extras del libro</span>
<i class="ri-arrow-right-circle-line fs-16 ms-2"></i>
</button>
</div>
</div>

View File

@ -1,4 +1,4 @@
<div>
<div id="content-datos-generales" class="animate-fadeInUpBounce">
<!-- Ribbon Shape -->
<div class="card ribbon-box border shadow-none mb-lg-0 material-shadow">
<div class="card-body">
@ -9,13 +9,21 @@
<div class="ribbon-content mt-4">
<div class="alert alert-danger alert-dismissible alert-label-icon rounded-label fade show material-shadow d-none"
role="alert" id="datos-generales-alert">
<i class="ri-error-warning-line label-icon"></i>
<strong th:text="#{presupuesto.errores-title}">Corrija los siguientes errores:</strong>
<ul class="mb-0" id="datos-generales-alert-list">
</ul>
</div>
<div class="px-2">
<div class="row">
<div class="col-sm-12">
<div class="mb-3">
<label for="titulo" class="form-label" th:text="#{presupuesto.titulo}">
>Título*</label>
<input type="text" class="form-control" id="titulo" placeholder="" value="">
<input type="text" class="form-control datos-generales-data" id="titulo" placeholder="" value="">
</div>
</div>
</div>
@ -24,13 +32,13 @@
<div class="col-sm-6">
<div class="mb-3">
<label for="autor" class="form-label" th:text="#{presupuesto.autor}">Autor</label>
<input type="text" class="form-control" id="autor" value="">
<input type="text" class="form-control datos-generales-data" id="autor" value="">
</div>
</div>
<div class="col-sm-6">
<div class="mb-3">
<label for="isbn" class="form-label" th:text="#{presupuesto.isbn}">ISBN</label>
<input type="text" class="form-control" id="isbn" value="">
<input type="text" class="form-control datos-generales-data" id="isbn" value="">
</div>
</div>
</div>
@ -38,67 +46,87 @@
<div class="col-sm-12">
<div class="row">
<div class="col-sm-3 mb-1">
<label for="tirada" class="form-label" th:text="#{presupuesto.tirada1}">Tirada 1*</label>
<input type="number" class="form-control" id="tirada" placeholder="" value="10">
<label for="tirada1" class="form-label" th:text="#{presupuesto.tirada1}">Tirada 1*</label>
<input type="number" class="form-control datos-generales-data" id="tirada1" placeholder="" value="10">
</div>
<div class="col-sm-3 mb-1">
<label for="tirada2" class="form-label" th:text="#{presupuesto.tirada2}">Tirada 2</label>
<input type="number" class="form-control" id="tirada" placeholder="" value="">
<input type="number" class="form-control datos-generales-data" id="tirada2" placeholder="" value="">
</div>
<div class="col-sm-3 mb-1">
<label for="tirada3" class="form-label" th:text="#{presupuesto.tirada3}">Tirada 3</label>
<input type="number" class="form-control" id="tirada" placeholder="" value="">
<input type="number" class="form-control datos-generales-data" id="tirada3" placeholder="" value="">
</div>
<div class="col-sm-3 mb-1">
<label for="tirada4" class="form-label" th:text="#{presupuesto.tirada4}">Tirada 4</label>
<input type="number" class="form-control" id="tirada" placeholder="" value="">
<input type="number" class="form-control datos-generales-data" id="tirada4" placeholder="" value="">
</div>
</div>
<p class="text-muted" th:text="#{presupuesto.tiradasPODnoPOD}">
<p class="text-muted" th:text="#{presupuesto.tiradasPODnoPOD(${pod})}">
No puede mezclar tiradas menores de 30 unidades con mayores de 30
unidades</p>
</div>
<div class="row justify-content-center">
<div class="col-sm-4">
<div class="mb-3">
<label for="formato" class="form-label" th:text="#{presupuesto.formato}">Formato</label>
<select class="form-control select2" id="formato">
<optgroup th:label="#{presupuesto.orientacion.vertical}">
<option value="148x210">148x210 (A5)</option>
<option value="115x170">115x170</option>
<option value="210x297">210x297 (A4)</option>
</optgroup>
<optgroup th:label="#{presupuesto.orientacion.cuadrado}">
<option value="210x210">210x210</option>
<option value="230x230">230x230</option>
</optgroup>
<optgroup th:label="#{presupuesto.orientacion.apaisado}">
<option value="210x148">210x148 (A5)</option>
<option value="240x170">240x170</option>
<option value="297x210">297x210 (A4)</option>
</optgroup>
</select>
<div class="col-sm-8">
</div>
<div class="mb-3">
<div class="form-check form-switch form-switch-left form-switch-md">
<input type="checkbox" class="form-check-input" id="checkbox-button">
<label for="checkbox-button" class="form-label"
th:text="#{presupuesto.formato-personalizado}">Formato
Personalizado</label>
<div class="row justify-content-center">
<div class="col-sm-4 mb-3 div-formato">
<label for="formato" class="form-label" th:text="#{presupuesto.formato}">Formato</label>
<select class="form-select select2 datos-generales-data" id="formato">
<optgroup th:label="#{presupuesto.orientacion.vertical}">
<option data-ancho="148" data-alto="210" value="148x210">148x210 (A5)</option>
<option data-ancho="120" data-alto="170" value="120x170">120x170</option>
<option data-ancho="210" data-alto="297" value="210x297">210x297 (A4)</option>
</optgroup>
<optgroup th:label="#{presupuesto.orientacion.cuadrado}">
<option data-ancho="210" data-alto="210" value="210x210">210x210</option>
<option data-ancho="230" data-alto="230" value="230x230">230x230</option>
</optgroup>
<optgroup th:label="#{presupuesto.orientacion.apaisado}">
<option data-ancho="210" data-alto="148" value="210x148">210x148 (A5)</option>
<option data-ancho="240" data-alto="170" value="240x170">240x170</option>
<option data-ancho="297" data-alto="210" value="297x210">297x210 (A4)</option>
</optgroup>
</select>
</div>
</div>
<div class="row justify-content-center div-formato-personalizado d-none">
<div class="col-3 col-sm-3 mb-1">
<label for="ancho" class="form-label" th:text="#{presupuesto.ancho}">Ancho (mm)</label>
<input type="number" class="form-control datos-generales-data" id="ancho" name="ancho" value="210"
th:attr="min=${ancho_alto_min}, max=${ancho_alto_max}">
</div>
<div class="col-3 col-sm-3 mb-1">
<label for="alto" class="form-label" th:text="#{presupuesto.alto}">Alto (mm)</label>
<input type="number" class="form-control datos-generales-data" id="alto" name="alto" value="297"
th:attr="min=${ancho_alto_min}, max=${ancho_alto_max}">
</div>
</div>
<div class="row justify-content-center mb-3">
<div class="col-sm-4 d-flex justify-content-center">
<div class="form-check form-switch form-switch-custom form-switch-primary mb-3">
<input type="checkbox" class="form-check-input form-switch-custom-primary datos-generales-data"
id="formato-personalizado" name="formato-personalizado">
<label for="formato-personalizado" class="form-label"
th:text="#{presupuesto.formato-personalizado}">
Formato Personalizado
</label>
</div>
</div>
</div>
</div>
</div>
<div class="row justify-content-center mb-2">
<div class="col-sm-2">
<label for="paginas-negro" class="form-label" th:text="#{presupuesto.paginas-negro}">Páginas
Negro</label>
<input type="number" step="2" class="form-control" id="paginas-negro" name="paginas-negro"
value="0">
<input type="number" step="2" class="form-control paginas datos-generales-data" id="paginas-negro"
name="paginas-negro" value="0">
<div class="form-text" th:text="#{presupuesto.siempre-pares}">
Siempre deben ser pares</div>
</div>
@ -110,8 +138,8 @@
<div class="col-sm-2">
<label for="paginas-color" class="form-label" th:text="#{presupuesto.paginas-color}">Páginas
Color</label>
<input type="number" step="2" class="form-control" id="paginas-color" name="paginas-color"
value="32">
<input type="number" step="2" class="form-control paginas datos-generales-data" id="paginas-color"
name="paginas-color" value="32">
<div class="form-text" th:text="#{presupuesto.siempre-pares}">
Siempre deben ser pares</div>
</div>
@ -126,6 +154,21 @@
<input disabled class="form-control" id="paginas" name="paginas" value="32">
</div>
</div>
<div id="div-posicion-paginas-color" class="row justify-content-center">
<div class="col-sm-9">
<label for="posicionPaginasColor" class="form-label" th:text="#{presupuesto.paginas-posicion}">
Posición páginas color
</label>
<input class="form-control datos-generales-data" id="posicionPaginasColor" value="">
<div class="form-text">
<p th:text="#{presupuesto.paginas-posicion-descripcion}">
Intruzca la posición separada por comas. Ej: 3,5,7 ó 4-10,20,155
</p>
</div>
</div>
</div>
</div>
</div>
@ -143,61 +186,64 @@
<div class="ribbon-content mt-4">
<div class="px-2">
<div class="row justify-content-center imagen-container-group">
<div class="d-flex flex-wrap justify-content-center gap-3 imagen-container-group">
<!-- Opción: Fresado -->
<div class="col-12 col-sm-6 col-md-4 d-flex justify-content-center mb-4 selected">
<div class="image-container imagen-selector" id="fresado">
<img class="image-presupuesto" src="/assets/images/imprimelibros/presupuestador/fresado.png"
alt="Fresado" />
<div class="form-text text-center">Fresado (a partir de 32 páginas)</div>
</div>
<div class="tipo-libro image-container imagen-selector selected datos-generales-data" id="fresado">
<input type="radio" name="tipoEncuadernacion" value="fresado" hidden>
<img class="image-presupuesto" src="/assets/images/imprimelibros/presupuestador/fresado.png"
alt="Fresado" />
<div class="form-text text-center" th:text="#{presupuesto.fresado-descripcion}">
Fresado (a partir de 32 páginas)</div>
</div>
<!-- Opción: Cosido -->
<div class="col-12 col-sm-6 col-md-4 d-flex justify-content-center mb-4">
<div class="image-container imagen-selector" id="cosido">
<img class="image-presupuesto" src="/assets/images/imprimelibros/presupuestador/cosido.png"
alt="Cosido" />
<div class="form-text text-center">Cosido (a partir de 32 páginas)</div>
</div>
<div class="tipo-libro image-container imagen-selector datos-generales-data" id="cosido">
<input type="radio" name="tipoEncuadernacion" value="cosido" hidden>
<img class="image-presupuesto" src="/assets/images/imprimelibros/presupuestador/cosido.png"
alt="Cosido" />
<div class="form-text text-center" th:text="#{presupuesto.cosido-descripcion}">
Cosido (a partir de 32 páginas)</div>
</div>
<!-- Opción: Grapado -->
<div class="col-12 col-sm-6 col-md-4 d-flex justify-content-center mb-4">
<div class="image-container imagen-selector" id="grapado">
<img class="image-presupuesto" src="/assets/images/imprimelibros/presupuestador/grapado.png"
alt="Grapado" />
<div class="form-text text-center">Grapado (entre 12 y 40 páginas)</div>
</div>
<div class="tipo-libro image-container imagen-selector datos-generales-data" id="grapado">
<input type="radio" name="tipoEncuadernacion" value="grapado" hidden>
<img class="image-presupuesto" src="/assets/images/imprimelibros/presupuestador/grapado.png"
alt="Grapado" />
<div class="form-text text-center" th:text="#{presupuesto.grapado-descripcion}">
Grapado (entre 12 y 40 páginas)</div>
</div>
<!-- Opción: Espiral -->
<div class="col-12 col-sm-6 col-md-4 d-flex justify-content-center mb-4">
<div class="image-container imagen-selector" id="espiral">
<img class="image-presupuesto" src="/assets/images/imprimelibros/presupuestador/espiral.png"
alt="Espiral" />
<div class="form-text text-center">Espiral (a partir de 20 páginas)</div>
</div>
<div class="tipo-libro image-container imagen-selector datos-generales-data" id="espiral">
<input type="radio" name="tipoEncuadernacion" value="espiral" hidden>
<img class="image-presupuesto" src="/assets/images/imprimelibros/presupuestador/espiral.png"
alt="Espiral" />
<div class="form-text text-center" th:text="#{presupuesto.espiral-descripcion}">
Espiral (a partir de 20 páginas)</div>
</div>
<!-- Opción: Wire-O -->
<div class="col-12 col-sm-6 col-md-4 d-flex justify-content-center mb-4">
<div class="image-container imagen-selector" id="wireo">
<img class="image-presupuesto" src="/assets/images/imprimelibros/presupuestador/wire-o.png"
alt="Wire-O" />
<div class="form-text text-center">Wire-O (a partir de 20 páginas)</div>
</div>
<div class="tipo-libro image-container imagen-selector datos-generales-data" id="wireo">
<input type="radio" name="tipoEncuadernacion" value="wireo" hidden>
<img class="image-presupuesto" src="/assets/images/imprimelibros/presupuestador/wire-o.png"
alt="Wire-O" />
<div class="form-text text-center" th:text="#{presupuesto.wire-o-descripcion}">
Wire-O (a partir de 20 páginas)</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex align-items-start gap-3 mt-3">
<button type="button" id="next-datos-generales" class="btn btn-primary btn-label right ms-auto">
<i class="ri-arrow-right-circle-line align-middle fs-16 ms-2"></i>
<label th:text="#{presupuesto.siguiente}">Siguiente</label>
</button>
<div class="d-flex align-items-center justify-content-center gap-3 mt-3">
<button type="button" id="next-datos-generales" class="btn btn-primary d-flex align-items-center ms-auto">
<span th:text="#{presupuesto.continuar-interior}">Continuar a diseño interior</span>
<i class="ri-arrow-right-circle-line fs-16 ms-2"></i>
</button>
</div>
</div>

View File

@ -0,0 +1,127 @@
<div id="content-interior" class="animate-fadeInUpBounce">
<!-- Ribbon Shape -->
<div class="card ribbon-box border shadow-none mb-lg-0 material-shadow">
<div class="card-body">
<div class="ribbon ribbon-primary ribbon-shape" th:text="#{presupuesto.tipo-encuadernacion}">Tipo de impresión
</div>
<h5 class="fs-14 text-end" th:text="#{presupuesto.tipo-encuadernacion-descripcion}">Elija entre calidad estándar
o premium</h5>
</div>
<div class="ribbon-content mt-4">
<div class="alert alert-danger alert-dismissible alert-label-icon rounded-label fade show material-shadow d-none"
role="alert" id="inside-alert">
<i class="ri-error-warning-line label-icon"></i>
<strong th:text="#{presupuesto.errores-title}">Corrija los siguientes errores:</strong>
<ul class="mb-0" id="inside-alert-list">
</ul>
</div>
<div id="div-opciones-color" class="row justify-content-center imagen-container-group">
</div>
</div>
</div>
<!-- End Ribbon Shape -->
<!-- Ribbon Shape -->
<div class="card ribbon-box border shadow-none mb-lg-0 material-shadow mt-4">
<div class="card-body">
<div class="ribbon ribbon-primary ribbon-shape" th:text="#{presupuesto.papel-interior}">Papel interior</div>
<h5 class="fs-14 text-end" th:text="#{presupuesto.papel-interior-descripcion}">
Papel y gramaje del interior</h5>
</div>
<div class="ribbon-content mt-4">
<div id="div-papel-interior" class="row justify-content-center imagen-container-group">
<div id="offset-blanco" data-sk-id="3" class="image-container imagen-selector selected">
<img class="image-presupuesto" src="/assets/images/imprimelibros/presupuestador/offset-blanco.png"
alt="">
<label for="titulo" class="form-label" th:text="#{presupuesto.offset-blanco}">
Offset Blanco
</label>
</div>
<div id="offset-ahuesado" data-sk-id="4" class="image-container imagen-selector">
<img class="image-presupuesto" src="/assets/images/imprimelibros/presupuestador/offset-ahuesado.png"
alt="">
<label class="form-label" th:text="#{presupuesto.offset-ahuesado}">
Offset Ahuesado
</label>
</div>
<div id="offset-ahuesado-volumen" data-sk-id="6" class="image-container imagen-selector">
<img class="image-presupuesto"
src="/assets/images/imprimelibros/presupuestador/offset-ahuesado-volumen.png" alt="">
<label class="form-label" th:text="#{presupuesto.offset-ahuesado-volumen}">
Offset Ahuesado Volumen
</label>
</div>
<div id="estucado-mate" data-sk-id="2" class="image-container imagen-selector">
<img class="image-presupuesto" src="/assets/images/imprimelibros/presupuestador/estucado-mate.png"
alt="">
<label class="form-label" th:text="#{presupuesto.estucado-mate}">
Estucado Mate
</label>
</div>
</div>
</div>
</div>
<!-- Ribbon Shape -->
<div class="card ribbon-box border shadow-none mb-lg-0 material-shadow mt-4">
<div class="card-body">
<div class="ribbon ribbon-primary ribbon-shape" th:text="#{presupuesto.gramaje-interior}">Gramaje interior
</div>
<h5 class="fs-14 text-end" th:text="#{presupuesto.gramaje-interior-descripcion}">
Gramaje del interior</h5>
</div>
<div class="ribbon-content mt-4">
<div id="div-gramaje-interior" class="hstack gap-2 justify-content-center flex-wrap">
<input type="radio" class="btn-check" data-gramaje="70" id="gramaje-70" name="gramaje-interior">
<label class="btn btn-outline-primary material-shadow gramaje-radio" for="gramaje-70">70</label>
<input type="radio" class="btn-check" data-gramaje="80" id="gramaje-80" name="gramaje-interior">
<label class="btn btn-outline-primary material-shadow gramaje-radio" for="gramaje-80">80</label>
<input type="radio" class="btn-check" data-gramaje="90" id="gramaje-90" checked name="gramaje-interior">
<label class="btn btn-outline-primary material-shadow gramaje-radio" for="gramaje-90">90</label>
<input type="radio" class="btn-check" data-gramaje="100" id="gramaje-100" name="gramaje-interior">
<label class="btn btn-outline-primary material-shadow gramaje-radio" for="gramaje-100">100</label>
<input type="radio" class="btn-check" data-gramaje="115" id="gramaje-115" name="gramaje-interior">
<label class="btn btn-outline-primary material-shadow gramaje-radio" for="gramaje-115">115</label>
<input type="radio" class="btn-check" data-gramaje="120" id="gramaje-120" name="gramaje-interior">
<label class="btn btn-outline-primary material-shadow gramaje-radio" for="gramaje-120">120</label>
<input type="radio" class="btn-check" data-gramaje="135" id="gramaje-135" name="gramaje-interior">
<label class="btn btn-outline-primary material-shadow gramaje-radio" for="gramaje-135">135</label>
<input type="radio" class="btn-check" data-gramaje="150" id="gramaje-150" name="gramaje-interior">
<label class="btn btn-outline-primary material-shadow gramaje-radio" for="gramaje-150">150</label>
<input type="radio" class="btn-check" data-gramaje="170" id="gramaje-170" name="gramaje-interior">
<label class="btn btn-outline-primary material-shadow gramaje-radio" for="gramaje-170">170</label>
</div>
</div>
</div>
<div class="d-flex justify-content-between align-items-center mt-4 w-100">
<button id="btn-prev-interior" type="button" class="btn btn-change-tab-interior btn-light d-flex align-items-center">
<i class=" ri-arrow-left-circle-line label-icon align-middle fs-16 me-2"></i>
<span th:text="#{presupuesto.volver-datos-generales}">Volver a datos generales</span>
</button>
<button id="btn-next-interior" type="button" class="btn btn-change-tab-interior btn-primary d-flex align-items-center">
<span th:text="#{presupuesto.continuar-cubierta}">Continuar a diseño cubierta</span>
<i class="ri-arrow-right-circle-line fs-16 ms-2"></i>
</button>
</div>
</div>

View File

@ -13,7 +13,7 @@
data-bs-toggle="pill" data-bs-target="#pills-general-data" type="button"
role="tab" aria-controls="pills-general-data" aria-selected="true">
<i
class="ri-information-fill fs-16 p-2 bg-soft-primary text-primary rounded-circle align-middle me-2"></i>
class="ri-information-line fs-16 p-2 bg-soft-primary text-primary rounded-circle align-middle me-2"></i>
<label th:text="#{presupuesto.datos-generales}">Datos Generales</label>
</button>
</li>
@ -40,8 +40,8 @@
data-bs-target="#pills-shipping" type="button" role="tab"
aria-controls="pills-shipping" aria-selected="false">
<i
class="ri-truck-line fs-16 p-2 bg-soft-primary text-primary rounded-circle align-middle me-2"></i>
<label th:text="#{presupuesto.envio}">Envío</label>
class="ri-add-box-line fs-16 p-2 bg-soft-primary text-primary rounded-circle align-middle me-2"></i>
<label th:text="#{presupuesto.extras}">Extras</label>
</button>
</li>
</ul>
@ -52,262 +52,24 @@
aria-labelledby="pills-general-data-tab">
<div th:include="~{imprimelibros/presupuestos/presupuestador-items/_datos-generales.html}"></div>
</div>
<!-- end tab pane -->
<div class="tab-pane fade" id="pills-bill-address" role="tabpanel"
aria-labelledby="pills-bill-address-tab">
<div>
<h5 class="mb-1">Shipping Information</h5>
<p class="text-muted mb-4">Please fill all information below</p>
</div>
<div class="mt-4">
<div class="d-flex align-items-center mb-2">
<div class="flex-grow-1">
<h5 class="fs-14 mb-0">Saved Address</h5>
</div>
<div class="flex-shrink-0">
<!-- Button trigger modal -->
<button type="button" class="btn btn-sm btn-info mb-3"
data-bs-toggle="modal" data-bs-target="#addAddressModal">
Add Address
</button>
</div>
</div>
<div class="row gy-3">
<div class="col-lg-4 col-sm-6">
<div class="form-check card-radio">
<input id="shippingAddress01" name="shippingAddress" type="radio"
class="form-check-input" checked>
<label class="form-check-label" for="shippingAddress01">
<span
class="mb-4 fw-semibold d-block text-muted text-uppercase">Home
Address</span>
<span class="fs-14 mb-2 d-block">Marcus Alfaro</span>
<span class="text-muted fw-normal text-wrap mb-1 d-block">4739
Bubby Drive Austin, TX 78729</span>
<span class="text-muted fw-normal d-block">Mo.
012-345-6789</span>
</label>
</div>
<div class="d-flex flex-wrap p-2 py-1 bg-light rounded-bottom border mt-n1">
<div>
<a href="#" class="d-block text-body p-1 px-2"
data-bs-toggle="modal" data-bs-target="#addAddressModal"><i
class="ri-pencil-fill text-muted align-bottom me-1"></i>
Edit</a>
</div>
<div>
<a href="#" class="d-block text-body p-1 px-2"
data-bs-toggle="modal" data-bs-target="#removeItemModal"><i
class="ri-delete-bin-fill text-muted align-bottom me-1"></i>
Remove</a>
</div>
</div>
</div>
<div class="col-lg-4 col-sm-6">
<div class="form-check card-radio">
<input id="shippingAddress02" name="shippingAddress" type="radio"
class="form-check-input">
<label class="form-check-label" for="shippingAddress02">
<span
class="mb-4 fw-semibold d-block text-muted text-uppercase">Office
Address</span>
<span class="fs-14 mb-2 d-block">James Honda</span>
<span class="text-muted fw-normal text-wrap mb-1 d-block">1246
Virgil Street Pensacola, FL 32501</span>
<span class="text-muted fw-normal d-block">Mo.
012-345-6789</span>
</label>
</div>
<div class="d-flex flex-wrap p-2 py-1 bg-light rounded-bottom border mt-n1">
<div>
<a href="#" class="d-block text-body p-1 px-2"
data-bs-toggle="modal" data-bs-target="#addAddressModal"><i
class="ri-pencil-fill text-muted align-bottom me-1"></i>
Edit</a>
</div>
<div>
<a href="#" class="d-block text-body p-1 px-2"
data-bs-toggle="modal" data-bs-target="#removeItemModal"><i
class="ri-delete-bin-fill text-muted align-bottom me-1"></i>
Remove</a>
</div>
</div>
</div>
</div>
<div class="mt-4">
<h5 class="fs-14 mb-3">Shipping Method</h5>
<div class="row g-4">
<div class="col-lg-6">
<div class="form-check card-radio">
<input id="shippingMethod01" name="shippingMethod" type="radio"
class="form-check-input" checked>
<label class="form-check-label" for="shippingMethod01">
<span
class="fs-20 float-end mt-2 text-wrap d-block fw-semibold">Free</span>
<span class="fs-14 mb-1 text-wrap d-block">Free
Delivery</span>
<span class="text-muted fw-normal text-wrap d-block">Expected
Delivery 3 to 5 Days</span>
</label>
</div>
</div>
<div class="col-lg-6">
<div class="form-check card-radio">
<input id="shippingMethod02" name="shippingMethod" type="radio"
class="form-check-input" checked>
<label class="form-check-label" for="shippingMethod02">
<span
class="fs-20 float-end mt-2 text-wrap d-block fw-semibold">$24.99</span>
<span class="fs-14 mb-1 text-wrap d-block">Express
Delivery</span>
<span class="text-muted fw-normal text-wrap d-block">Delivery
within 24hrs.</span>
</label>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex align-items-start gap-3 mt-4">
<button type="button" class="btn btn-light btn-label previestab"
data-previous="pills-bill-info-tab"><i
class="ri-arrow-left-line label-icon align-middle fs-16 me-2"></i>Back
to Personal Info</button>
<button type="button" class="btn btn-primary btn-label right ms-auto nexttab"
data-nexttab="pills-payment-tab"><i
class="ri-bank-card-line label-icon align-middle fs-16 ms-2"></i>Continue
to Payment</button>
</div>
<div class="tab-pane fade" id="pills-inside" role="tabpanel"
aria-labelledby="pills-inside-tab">
<div th:include="~{imprimelibros/presupuestos/presupuestador-items/_interior.html}"></div>
</div>
<!-- end tab pane -->
<div class="tab-pane fade" id="pills-payment" role="tabpanel"
aria-labelledby="pills-payment-tab">
<div>
<h5 class="mb-1">Payment Selection</h5>
<p class="text-muted mb-4">Please select and enter your billing information</p>
</div>
<div class="row g-4">
<div class="col-lg-4 col-sm-6">
<div data-bs-toggle="collapse" data-bs-target="#paymentmethodCollapse.show"
aria-expanded="false" aria-controls="paymentmethodCollapse">
<div class="form-check card-radio">
<input id="paymentMethod01" name="paymentMethod" type="radio"
class="form-check-input">
<label class="form-check-label" for="paymentMethod01">
<span class="fs-16 text-muted me-2"><i
class="ri-paypal-fill align-bottom"></i></span>
<span class="fs-14 text-wrap">Paypal</span>
</label>
</div>
</div>
</div>
<div class="col-lg-4 col-sm-6">
<div data-bs-toggle="collapse" data-bs-target="#paymentmethodCollapse"
aria-expanded="true" aria-controls="paymentmethodCollapse">
<div class="form-check card-radio">
<input id="paymentMethod02" name="paymentMethod" type="radio"
class="form-check-input" checked>
<label class="form-check-label" for="paymentMethod02">
<span class="fs-16 text-muted me-2"><i
class="ri-bank-card-fill align-bottom"></i></span>
<span class="fs-14 text-wrap">Credit / Debit Card</span>
</label>
</div>
</div>
</div>
<div class="col-lg-4 col-sm-6">
<div data-bs-toggle="collapse" data-bs-target="#paymentmethodCollapse.show"
aria-expanded="false" aria-controls="paymentmethodCollapse">
<div class="form-check card-radio">
<input id="paymentMethod03" name="paymentMethod" type="radio"
class="form-check-input">
<label class="form-check-label" for="paymentMethod03">
<span class="fs-16 text-muted me-2"><i
class="ri-money-dollar-box-fill align-bottom"></i></span>
<span class="fs-14 text-wrap">Cash on Delivery</span>
</label>
</div>
</div>
</div>
</div>
<div class="collapse show" id="paymentmethodCollapse">
<div class="card p-4 border shadow-none mb-0 mt-4">
<div class="row gy-3">
<div class="col-md-12">
<label for="cc-name" class="form-label">Name on card</label>
<input type="text" class="form-control" id="cc-name"
placeholder="Enter name">
<small class="text-muted">Full name as displayed on card</small>
</div>
<div class="col-md-6">
<label for="cc-number" class="form-label">Credit card number</label>
<input type="text" class="form-control" id="cc-number"
placeholder="xxxx xxxx xxxx xxxx">
</div>
<div class="col-md-3">
<label for="cc-expiration" class="form-label">Expiration</label>
<input type="text" class="form-control" id="cc-expiration"
placeholder="MM/YY">
</div>
<div class="col-md-3">
<label for="cc-cvv" class="form-label">CVV</label>
<input type="text" class="form-control" id="cc-cvv" placeholder="xxx">
</div>
</div>
</div>
<div class="text-muted mt-2 fst-italic">
<i data-feather="lock" class="text-muted icon-xs"></i> Your transaction is
secured with SSL encryption
</div>
</div>
<div class="d-flex align-items-start gap-3 mt-4">
<button type="button" class="btn btn-light btn-label previestab"
data-previous="pills-bill-address-tab"><i
class="ri-arrow-left-line label-icon align-middle fs-16 me-2"></i>Back
to Shipping</button>
<button type="button" class="btn btn-secondary btn-label right ms-auto nexttab"
data-nexttab="pills-finish-tab"><i
class="ri-shopping-basket-line label-icon align-middle fs-16 ms-2"></i>Complete
Order</button>
</div>
<div class="tab-pane fade" id="pills-cover" role="tabpanel"
aria-labelledby="pills-cover-tab">
<div th:include="~{imprimelibros/presupuestos/presupuestador-items/_cubierta.html}"></div>
</div>
<!-- end tab pane -->
<div class="tab-pane fade" id="pills-finish" role="tabpanel"
aria-labelledby="pills-finish-tab">
<div class="text-center py-5">
<div class="mb-4">
<lord-icon src="https://cdn.lordicon.com/lupuorrc.json" trigger="loop"
colors="primary:#0ab39c,secondary:#405189"
style="width:120px;height:120px"></lord-icon>
</div>
<h5>Thank you ! Your Order is Completed !</h5>
<p class="text-muted">You will receive an order confirmation email with details
of your order.</p>
<h5 class="fw-semibold">Order ID: <a href="/apps-ecommerce-order-details"
class="text-decoration-underline">VZ2451</a></h5>
</div>
</div>
<!-- end tab pane -->
</div>
<!-- end tab content -->
</form>
@ -318,6 +80,7 @@
</div>
<!-- end col -->
<!-- Order Summary -->
<div class="col-xl-4">
<div class="card">
<div class="card-header">