añadida validacion en el backend para datos generales

This commit is contained in:
Jaime Jiménez
2025-07-28 13:03:34 +02:00
parent 14f6633b83
commit 8b34d6dca9
44 changed files with 1138 additions and 308 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

@ -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/validar/**")
.permitAll()
.anyRequest().authenticated())
.csrf(csrf -> csrf
.ignoringRequestMatchers("/presupuesto/validar/**"))
.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;
if (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

@ -1,24 +1,46 @@
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"));
}
return "imprimelibros/home";
}
}

View File

@ -0,0 +1,53 @@
package com.imprimelibros.erp.controller;
import org.springframework.web.bind.annotation.RestController;
import com.imprimelibros.erp.service.PresupuestoService;
import java.util.HashMap;
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.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.PostMapping;
import jakarta.validation.Valid; // o javax.validation.Valid según tu versión
import java.util.Map;
import com.imprimelibros.erp.config.validation.PresupuestoValidationGroups;
import com.imprimelibros.erp.entity.Presupuesto;
@RestController
@RequestMapping("/presupuesto")
public class PresupuestoController {
@Autowired
protected PresupuestoService presupuestoService;
@PostMapping("/validar/datos-generales")
public ResponseEntity<?> validarDatosGenerales(
@Validated(PresupuestoValidationGroups.DatosGenerales.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()));
// errores globales (como tu @ConsistentTiradas)
result.getGlobalErrors().forEach(error -> errores.put("global", error.getDefaultMessage()));
if (!errores.isEmpty()) {
return ResponseEntity.badRequest().body(errores);
}
return ResponseEntity.ok().build();
}
}

View File

@ -0,0 +1,153 @@
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.ConsistentTiradas;
import com.imprimelibros.erp.config.validation.Par;
@ConsistentTiradas(groups = PresupuestoValidationGroups.DatosGenerales.class)
@Entity
@Table(name = "presupuesto")
public class Presupuesto {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Otros campos del presupuesto
@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;
@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;
@NotNull(message = "El papel interior no puede estar vacío", groups = PresupuestoValidationGroups.Interior.class)
@Column(name = "papel_interior")
private Integer papelInterior;
// 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 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 Integer getPapelInterior() {
return papelInterior;
}
public void setPapelInterior(Integer papelInterior) {
this.papelInterior = papelInterior;
}
}

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,18 @@
package com.imprimelibros.erp.service;
import org.springframework.stereotype.Service;
@Service
public class PresupuestoService {
public boolean validateDatosGenerales(int[] tiradas) {
// Implement the logic to validate the tiradas
// For example, check if all tiradas are positive integers
for (int tirada : tiradas) {
if (tirada <= 0) {
return false; // Invalid tirada found
}
}
return true; // All tiradas are valid
}
}

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,7 +13,7 @@ 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
@ -26,5 +26,70 @@ presupuesto.paginas-negro=Páginas Negro
presupuesto.paginas-color=Páginas Color
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-impresion=Tipo de impresión
presupuesto.tipo-impresion-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.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

View File

@ -18,6 +18,8 @@
border-radius: 8px;
padding: 8px;
margin-inline: 5px;
transition: border 0.3s ease;
}
@ -58,3 +60,14 @@
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 */
}

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: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 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

@ -0,0 +1,111 @@
class PresupuestoCliente {
constructor() {
// pestaña datos generales
this.titulo = $('#titulo');
this.autor = $('#autor');
this.isbn = $('#isbn');
this.tirada1 = $('#tirada1');
this.tirada2 = $('#tirada2');
this.tirada3 = $('#tirada3');
this.tirada4 = $('#tirada4');
this.paginasNegro = $('#paginas-negro');
this.paginasColor = $('#paginas-color')
this.btn_next_datos_generales = $('#next-datos-generales');
this.datos_generales_alert = $('#datos-generales-alert');
// pestaña cubierta
this.btn_plantilla_cubierta = $('#btn-plantilla-cubierta');
this.btn_impresion_cubierta_help = $('#impresion-cubierta-help');
}
init() {
this.initButtonsEventListeners();
}
initButtonsEventListeners() {
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
});
});
this.btn_next_datos_generales.on('click', () => {
this.nextDatosGenerales();
});
}
nextDatosGenerales() {
const data = {
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(),
paginasNegro: this.paginasNegro.val(),
paginasColor: this.paginasColor.val()
}
$.ajax({
url: '/presupuesto/validar/datos-generales',
type: 'POST',
data: data,
success: (response) => {
this.datos_generales_alert.addClass('d-none');
},
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') {
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);
}
});
}
}
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

@ -35,7 +35,11 @@
<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/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="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/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/sweetalert2/sweetalert2.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/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,216 @@
<div>
<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">
<div id="tapa-blanda" class="image-container imagen-selector selected">
<img class="image-presupuesto" src="/assets/images/imprimelibros/presupuestador/tapa-blanda.jpg"
alt="">
<label for="titulo" class="form-label" th:text="#{presupuesto.tapa-blanda}">
Tapa blanda
</label>
</div>
<div id="tapa-dura" class="image-container imagen-selector">
<img class="image-presupuesto"
src="/assets/images/imprimelibros/presupuestador/tapa-dura-lomo-recto.jpg" alt="">
<label class="form-label" th:text="#{presupuesto.tapa-dura}">
Tapa dura
</label>
</div>
<div id="tapa-dura-lomo-redondo" class="image-container imagen-selector">
<img class="image-presupuesto"
src="/assets/images/imprimelibros/presupuestador/tapa-dura-lomo-redondo.jpg" 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">
<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">
<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 class="col-auto mb-3">
<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">
<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 class="row justify-content-center imagen-container-group">
<div id="cartulina-grafica-cubierta" data-sk-id="3" class="image-container imagen-selector selected">
<img class="image-presupuesto"
src="/assets/images/imprimelibros/presupuestador/cartulina-grafica.png" alt="">
<label for="titulo" class="form-label" th:text="#{presupuesto.cartulina-grafica-cubierta}">
Cartulina gráfica estucada a una cara
</label>
</div>
<div id="estucado-mate-cubierta" data-sk-id="2" class="image-container imagen-selector">
<img class="image-presupuesto"
src="/assets/images/imprimelibros/presupuestador/estucado-mate-cubierta.png" alt="">
<label class="form-label" th:text="#{presupuesto.estucado-mate}">
Estucado Mate
</label>
</div>
</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 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

@ -9,6 +9,14 @@
<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">
@ -38,23 +46,23 @@
<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" 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" 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" 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" 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>
@ -63,7 +71,7 @@
<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">
<select class="form-select select2" id="formato">
<optgroup th:label="#{presupuesto.orientacion.vertical}">
<option value="148x210">148x210 (A5)</option>
<option value="115x170">115x170</option>
@ -82,9 +90,10 @@
</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"
<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"
id="formato-personalizado" name="formato-personalizado">
<label for="formato-personalizado" class="form-label"
th:text="#{presupuesto.formato-personalizado}">Formato
Personalizado</label>
</div>
@ -146,11 +155,12 @@
<div class="row justify-content-center 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">
<div class="col-12 col-sm-6 col-md-4 d-flex justify-content-center mb-4">
<div class="image-container imagen-selector selected" 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 class="form-text text-center" th:text="#{presupuesto.fresado-descripcion}">
Fresado (a partir de 32 páginas)</div>
</div>
</div>
@ -159,7 +169,8 @@
<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 class="form-text text-center" th:text="#{presupuesto.cosido-descripcion}">
Cosido (a partir de 32 páginas)</div>
</div>
</div>
@ -168,7 +179,8 @@
<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 class="form-text text-center" th:text="#{presupuesto.grapado-descripcion}">
Grapado (entre 12 y 40 páginas)</div>
</div>
</div>
@ -177,7 +189,8 @@
<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 class="form-text text-center" th:text="#{presupuesto.espiral-descripcion}">
Espiral (a partir de 20 páginas)</div>
</div>
</div>
@ -186,7 +199,8 @@
<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 class="form-text text-center" th:text="#{presupuesto.wire-o-descripcion}">
Wire-O (a partir de 20 páginas)</div>
</div>
</div>
@ -194,10 +208,11 @@
</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,148 @@
<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-impresion}">Tipo de impresión
</div>
<h5 class="fs-14 text-end" th:text="#{presupuesto.tipo-impresion-descripcion}">Elija entre calidad estándar
o premium</h5>
</div>
<div class="ribbon-content mt-4">
<div class="row justify-content-center imagen-container-group">
<div id="negroEstandar" class="image-container imagen-selector selected">
<img class="image-presupuesto" src="/assets/images/imprimelibros/presupuestador/blancoYnegro.png"
alt="Negro">
<label for="titulo" class="form-label" th:text="#{presupuesto.blanco-negro}">
Blanco y Negro
</label>
</div>
<div id="negroPremium" class="image-container imagen-selector">
<img class="image-presupuesto" src="/assets/images/imprimelibros/presupuestador/negroFoto.png"
alt="NegroPremium">
<label class="form-label" th:text="#{presupuesto.blanco-negro-premium}">
Blanco y Negro Premium
</label>
</div>
<div id="colorEstandar" class="image-container imagen-selector">
<img class="image-presupuesto" src="/assets/images/imprimelibros/presupuestador/color.png"
alt="Color">
<label class="form-label" th:text="#{presupuesto.color}">
Color
</label>
</div>
<div id="colorPremium" class="image-container imagen-selector">
<img class="image-presupuesto" src="/assets/images/imprimelibros/presupuestador/colorFoto.png"
alt="ColorPremium">
<label class="form-label" th:text="#{presupuesto.color-premium}">
Color Premium
</label>
</div>
</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 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 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 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-datos-generales}">Volver a datos generales</span>
</button>
<button type="button" class="btn 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">