mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-01-28 22:58:49 +00:00
guardando presupuestos anonimos
This commit is contained in:
@ -46,7 +46,6 @@ public abstract class AbstractAuditedEntity {
|
|||||||
@Column(name = "deleted_at")
|
@Column(name = "deleted_at")
|
||||||
private Instant deletedAt;
|
private Instant deletedAt;
|
||||||
|
|
||||||
@LastModifiedBy
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
@JoinColumn(name = "deleted_by")
|
@JoinColumn(name = "deleted_by")
|
||||||
private User deletedBy;
|
private User deletedBy;
|
||||||
|
|||||||
@ -3,41 +3,45 @@ package com.imprimelibros.erp.config;
|
|||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.data.domain.AuditorAware;
|
import org.springframework.data.domain.AuditorAware;
|
||||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
|
||||||
|
|
||||||
import com.imprimelibros.erp.users.User;
|
import com.imprimelibros.erp.users.User;
|
||||||
import com.imprimelibros.erp.users.UserDao;
|
import com.imprimelibros.erp.users.UserDetailsImpl; // tu implementación
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableJpaAuditing(auditorAwareRef = "auditorAware")
|
@EnableJpaAuditing(auditorAwareRef = "auditorAware")
|
||||||
public class JpaAuditConfig {
|
public class JpaAuditConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public AuditorAware<User> auditorAware(UserDao userDao) {
|
public AuditorAware<User> auditorAware(EntityManager em) {
|
||||||
return () -> {
|
return () -> {
|
||||||
var auth = SecurityContextHolder.getContext().getAuthentication();
|
var ctx = SecurityContextHolder.getContext();
|
||||||
if (auth == null || !auth.isAuthenticated())
|
if (ctx == null) return Optional.empty();
|
||||||
return Optional.empty();
|
|
||||||
|
var auth = ctx.getAuthentication();
|
||||||
|
if (auth == null || !auth.isAuthenticated()) return Optional.empty();
|
||||||
|
|
||||||
Object principal = auth.getPrincipal();
|
Object principal = auth.getPrincipal();
|
||||||
|
Long userId = null;
|
||||||
|
|
||||||
if (principal instanceof User u)
|
// Tu UserDetailsImpl ya tiene el id
|
||||||
return Optional.of(u);
|
if (principal instanceof UserDetailsImpl udi) {
|
||||||
|
userId = udi.getId();
|
||||||
if (principal instanceof UserDetails ud) {
|
|
||||||
return userDao.findByUserNameIgnoreCase(ud.getUsername());
|
|
||||||
}
|
}
|
||||||
|
// Si a veces pones el propio User como principal:
|
||||||
if (principal instanceof String username && !"anonymousUser".equals(username)) {
|
else if (principal instanceof User u && u.getId() != null) {
|
||||||
return userDao.findByUserNameIgnoreCase(username);
|
userId = u.getId();
|
||||||
}
|
}
|
||||||
|
// ⚠️ NO hagas consultas aquí (nada de userDao.findBy...).
|
||||||
|
if (userId == null) return Optional.empty();
|
||||||
|
|
||||||
return Optional.empty();
|
// Devuelve una referencia gestionada (NO hace SELECT ni fuerza flush)
|
||||||
|
return Optional.of(em.getReference(User.class, userId));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,13 +5,13 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.context.MessageSource;
|
import org.springframework.context.MessageSource;
|
||||||
@ -33,7 +33,7 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import jakarta.validation.Validator;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
@ -48,12 +48,12 @@ import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
|||||||
import com.imprimelibros.erp.presupuesto.service.PresupuestoService;
|
import com.imprimelibros.erp.presupuesto.service.PresupuestoService;
|
||||||
import com.imprimelibros.erp.presupuesto.validation.PresupuestoValidationGroups;
|
import com.imprimelibros.erp.presupuesto.validation.PresupuestoValidationGroups;
|
||||||
import com.imprimelibros.erp.users.UserDao;
|
import com.imprimelibros.erp.users.UserDao;
|
||||||
import com.imprimelibros.erp.users.User;
|
|
||||||
import com.imprimelibros.erp.users.UserDetailsImpl;
|
import com.imprimelibros.erp.users.UserDetailsImpl;
|
||||||
import com.imprimelibros.erp.presupuesto.service.PresupuestoFormDataMapper;
|
import com.imprimelibros.erp.presupuesto.service.PresupuestoFormDataMapper;
|
||||||
import com.imprimelibros.erp.presupuesto.service.PresupuestoFormDataMapper.PresupuestoFormDataDto;
|
import com.imprimelibros.erp.presupuesto.service.PresupuestoFormDataMapper.PresupuestoFormDataDto;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.validation.ConstraintViolation;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@ -71,6 +71,9 @@ public class PresupuestoController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
protected MessageSource messageSource;
|
protected MessageSource messageSource;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private Validator validator;
|
||||||
|
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final TranslationService translationService;
|
private final TranslationService translationService;
|
||||||
private final PresupuestoDatatableService dtService;
|
private final PresupuestoDatatableService dtService;
|
||||||
@ -472,6 +475,8 @@ public class PresupuestoController {
|
|||||||
HttpServletRequest request) {
|
HttpServletRequest request) {
|
||||||
|
|
||||||
Presupuesto p = objectMapper.convertValue(body.get("presupuesto"), Presupuesto.class);
|
Presupuesto p = objectMapper.convertValue(body.get("presupuesto"), Presupuesto.class);
|
||||||
|
Boolean save = objectMapper.convertValue(body.get("save"), Boolean.class);
|
||||||
|
String mode = objectMapper.convertValue(body.get("mode"), String.class);
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
List<Map<String, Object>> serviciosList = (List<Map<String, Object>>) body.getOrDefault("servicios", List.of());
|
List<Map<String, Object>> serviciosList = (List<Map<String, Object>>) body.getOrDefault("servicios", List.of());
|
||||||
@ -479,7 +484,7 @@ public class PresupuestoController {
|
|||||||
String sessionId = request.getSession(true).getId();
|
String sessionId = request.getSession(true).getId();
|
||||||
String ip = request.getRemoteAddr();
|
String ip = request.getRemoteAddr();
|
||||||
|
|
||||||
var resumen = presupuestoService.getResumenPublico(p, serviciosList, locale, sessionId, ip);
|
var resumen = presupuestoService.getResumen(p, serviciosList, save, mode, locale, sessionId, ip);
|
||||||
|
|
||||||
return ResponseEntity.ok(resumen);
|
return ResponseEntity.ok(resumen);
|
||||||
}
|
}
|
||||||
@ -498,7 +503,15 @@ public class PresupuestoController {
|
|||||||
"app.yes",
|
"app.yes",
|
||||||
"app.cancelar",
|
"app.cancelar",
|
||||||
"presupuesto.delete.ok.title",
|
"presupuesto.delete.ok.title",
|
||||||
"presupuesto.delete.ok.text");
|
"presupuesto.delete.ok.text",
|
||||||
|
"presupuesto.add.tipo",
|
||||||
|
"presupuesto.add.anonimo",
|
||||||
|
"presupuesto.add.cliente",
|
||||||
|
"presupuesto.add.next",
|
||||||
|
"presupuesto.add.cancel",
|
||||||
|
"presupuesto.add.select-client",
|
||||||
|
"presupuesto.add.error.options",
|
||||||
|
"presupuesto.add.error.options-client");
|
||||||
|
|
||||||
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||||
model.addAttribute("languageBundle", translations);
|
model.addAttribute("languageBundle", translations);
|
||||||
@ -557,6 +570,35 @@ public class PresupuestoController {
|
|||||||
return "imprimelibros/presupuestos/presupuesto-form";
|
return "imprimelibros/presupuestos/presupuesto-form";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping(value = { "/add/{mode}", "/add/{mode}/{cliente_id}", "/add2/{cliente_id}" })
|
||||||
|
public String getPresupuestoEditForm(
|
||||||
|
@PathVariable(name = "mode", required = false) String mode,
|
||||||
|
@PathVariable(name = "cliente_id", required = false) Long clienteId,
|
||||||
|
RedirectAttributes redirectAttributes,
|
||||||
|
Model model,
|
||||||
|
Authentication authentication,
|
||||||
|
Locale locale) {
|
||||||
|
|
||||||
|
List<String> keys = List.of(
|
||||||
|
"presupuesto.plantilla-cubierta",
|
||||||
|
"presupuesto.plantilla-cubierta-text",
|
||||||
|
"presupuesto.impresion-cubierta",
|
||||||
|
"presupuesto.impresion-cubierta-help");
|
||||||
|
|
||||||
|
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||||
|
model.addAttribute("languageBundle", translations);
|
||||||
|
model.addAttribute("pod", variableService.getValorEntero("POD"));
|
||||||
|
model.addAttribute("ancho_alto_min", variableService.getValorEntero("ancho_alto_min"));
|
||||||
|
model.addAttribute("ancho_alto_max", variableService.getValorEntero("ancho_alto_max"));
|
||||||
|
|
||||||
|
model.addAttribute("appMode", "add");
|
||||||
|
if (!mode.equals("public")) {
|
||||||
|
model.addAttribute("cliente_id", clienteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "imprimelibros/presupuestos/presupuesto-form";
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping(value = "/api/get", produces = "application/json")
|
@GetMapping(value = "/api/get", produces = "application/json")
|
||||||
public ResponseEntity<PresupuestoFormDataDto> getPresupuesto(
|
public ResponseEntity<PresupuestoFormDataDto> getPresupuesto(
|
||||||
@RequestParam("id") Long id, Authentication authentication) {
|
@RequestParam("id") Long id, Authentication authentication) {
|
||||||
@ -588,51 +630,102 @@ public class PresupuestoController {
|
|||||||
@Transactional
|
@Transactional
|
||||||
public ResponseEntity<?> delete(@PathVariable Long id, Authentication auth, Locale locale) {
|
public ResponseEntity<?> delete(@PathVariable Long id, Authentication auth, Locale locale) {
|
||||||
|
|
||||||
Presupuesto p = presupuestoRepository.findById(id)
|
Presupuesto p = presupuestoRepository.findById(id).orElse(null);
|
||||||
.orElseThrow(() -> new ResponseStatusException(
|
if (p == null) {
|
||||||
HttpStatus.NOT_FOUND,
|
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||||
messageSource.getMessage("presupuesto.error.not-found", null, locale)));
|
.body(Map.of("message", messageSource.getMessage("presupuesto.error.not-found", null, locale)));
|
||||||
|
}
|
||||||
|
|
||||||
boolean isUser = auth != null && auth.getAuthorities().stream()
|
boolean isUser = auth != null && auth.getAuthorities().stream()
|
||||||
.anyMatch(a -> a.getAuthority().equals("ROLE_USER"));
|
.anyMatch(a -> a.getAuthority().equals("ROLE_USER"));
|
||||||
|
|
||||||
// compara por IDs (no uses equals entre tipos distintos)
|
|
||||||
Long ownerId = p.getUser() != null ? p.getUser().getId() : null;
|
Long ownerId = p.getUser() != null ? p.getUser().getId() : null;
|
||||||
|
|
||||||
User currentUser = null;
|
|
||||||
Long currentUserId = null;
|
Long currentUserId = null;
|
||||||
if (auth != null && auth.getPrincipal() instanceof UserDetailsImpl udi) {
|
if (auth != null && auth.getPrincipal() instanceof UserDetailsImpl udi) {
|
||||||
currentUserId = udi.getId();
|
currentUserId = udi.getId();
|
||||||
currentUser = userRepo.findById(currentUserId).orElse(null);
|
|
||||||
} else if (auth != null) {
|
} else if (auth != null) {
|
||||||
currentUserId = userRepo.findIdByUserNameIgnoreCase(auth.getName()).orElse(null); // fallback
|
currentUserId = userRepo.findIdByUserNameIgnoreCase(auth.getName()).orElse(null);
|
||||||
currentUser = userRepo.findById(currentUserId).orElse(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isOwner = ownerId != null && ownerId.equals(currentUserId);
|
boolean isOwner = ownerId != null && ownerId.equals(currentUserId);
|
||||||
|
|
||||||
if (isUser && !isOwner) {
|
if (isUser && !isOwner) {
|
||||||
throw new ResponseStatusException(
|
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||||
HttpStatus.FORBIDDEN,
|
.body(Map.of("message",
|
||||||
messageSource.getMessage("presupuesto.error.delete-permission-denied", null, locale));
|
messageSource.getMessage("presupuesto.error.delete-permission-denied", null, locale)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (p.getEstado() != null && !p.getEstado().equals(Presupuesto.Estado.borrador)) {
|
if (p.getEstado() != null && !p.getEstado().equals(Presupuesto.Estado.borrador)) {
|
||||||
throw new ResponseStatusException(
|
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||||
HttpStatus.FORBIDDEN,
|
.body(Map.of("message",
|
||||||
messageSource.getMessage("presupuesto.error.delete-not-draft", null, locale));
|
messageSource.getMessage("presupuesto.error.delete-not-draft", null, locale)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// SOFT DELETE (no uses deleteById)
|
try {
|
||||||
p.setDeleted(true);
|
p.setDeleted(true);
|
||||||
p.setDeletedAt(Instant.now());
|
p.setDeletedAt(Instant.now());
|
||||||
p.setDeletedBy(currentUser);
|
|
||||||
|
|
||||||
presupuestoRepository.save(p);
|
if (auth != null && auth.getPrincipal() instanceof UserDetailsImpl udi) {
|
||||||
return ResponseEntity.ok(Map.of("message",
|
p.setDeletedBy(userRepo.getReferenceById(udi.getId()));
|
||||||
messageSource.getMessage("presupuesto.exito.eliminado", null, locale)));
|
} else if (auth != null) {
|
||||||
|
userRepo.findByUserNameIgnoreCase(auth.getName()).ifPresent(p::setDeletedBy);
|
||||||
|
}
|
||||||
|
presupuestoRepository.saveAndFlush(p);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(Map.of("message",
|
||||||
|
messageSource.getMessage("presupuesto.exito.eliminado", null, locale)));
|
||||||
|
|
||||||
|
} catch (Exception ex) {
|
||||||
|
// Devuelve SIEMPRE algo en el catch
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
.body(Map.of("message",
|
||||||
|
messageSource.getMessage("presupuesto.error.delete-internal-error", null, locale),
|
||||||
|
"detail",
|
||||||
|
ex.getClass().getSimpleName() + ": " + (ex.getMessage() != null ? ex.getMessage() : "")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(path="/save")
|
||||||
|
public ResponseEntity<?> save(
|
||||||
|
@RequestBody Map<String, Object> body,
|
||||||
|
Locale locale, HttpServletRequest request) {
|
||||||
|
|
||||||
|
Presupuesto presupuesto = objectMapper.convertValue(body.get("presupuesto"), Presupuesto.class);
|
||||||
|
Long id = objectMapper.convertValue(body.get("id"), Long.class);
|
||||||
|
String mode = objectMapper.convertValue(body.get("mode"), String.class);
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<Map<String, Object>> serviciosList = (List<Map<String, Object>>) body.getOrDefault("servicios", List.of());
|
||||||
|
|
||||||
|
Set<ConstraintViolation<Presupuesto>> violations = validator.validate(presupuesto,
|
||||||
|
PresupuestoValidationGroups.All.class);
|
||||||
|
|
||||||
|
if (!violations.isEmpty()) {
|
||||||
|
Map<String, String> errores = new HashMap<>();
|
||||||
|
for (ConstraintViolation<Presupuesto> v : violations) {
|
||||||
|
String campo = v.getPropertyPath().toString();
|
||||||
|
String mensaje = messageSource.getMessage(v.getMessage().replace("{", "").replace("}", ""), null, locale);
|
||||||
|
errores.put(campo, mensaje);
|
||||||
|
}
|
||||||
|
return ResponseEntity.badRequest().body(errores);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (mode.equals("public")) {
|
||||||
|
var resumen = presupuestoService.getTextosResumen(presupuesto, serviciosList, locale);
|
||||||
|
presupuesto = presupuestoService.generateTotalizadores(presupuesto, serviciosList, resumen, locale);
|
||||||
|
if (id != null) {
|
||||||
|
presupuesto.setId(id); // para que actualice, no cree uno nuevo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map<String, Object> saveResult = presupuestoService.guardarPresupuesto(presupuesto);
|
||||||
|
return ResponseEntity.ok(Map.of("id", saveResult.get("presupuesto_id"),
|
||||||
|
"message", messageSource.getMessage("presupuesto.exito.guardado", null, locale)));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
.body(Map.of("message",
|
||||||
|
messageSource.getMessage("presupuesto.error.save-internal-error", null, locale),
|
||||||
|
"detail",
|
||||||
|
ex.getClass().getSimpleName() + ": " + (ex.getMessage() != null ? ex.getMessage() : "")));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,10 +8,11 @@ import java.util.Map;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.text.NumberFormat;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.context.MessageSource;
|
import org.springframework.context.MessageSource;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@ -38,6 +39,8 @@ import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
|||||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoCubierta;
|
import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoCubierta;
|
||||||
import com.imprimelibros.erp.presupuesto.maquetacion.MaquetacionMatricesRepository;
|
import com.imprimelibros.erp.presupuesto.maquetacion.MaquetacionMatricesRepository;
|
||||||
import com.imprimelibros.erp.presupuesto.marcapaginas.MarcapaginasRepository;
|
import com.imprimelibros.erp.presupuesto.marcapaginas.MarcapaginasRepository;
|
||||||
|
import com.imprimelibros.erp.users.UserDao;
|
||||||
|
import com.imprimelibros.erp.users.UserDetailsImpl;
|
||||||
import com.imprimelibros.erp.externalApi.skApiClient;
|
import com.imprimelibros.erp.externalApi.skApiClient;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@ -65,13 +68,15 @@ public class PresupuestoService {
|
|||||||
private final PresupuestoFormatter presupuestoFormatter;
|
private final PresupuestoFormatter presupuestoFormatter;
|
||||||
private final skApiClient apiClient;
|
private final skApiClient apiClient;
|
||||||
private final GeoIpService geoIpService;
|
private final GeoIpService geoIpService;
|
||||||
|
private final UserDao userRepo;
|
||||||
|
|
||||||
public PresupuestoService(PresupuestadorItems presupuestadorItems, PresupuestoFormatter presupuestoFormatter,
|
public PresupuestoService(PresupuestadorItems presupuestadorItems, PresupuestoFormatter presupuestoFormatter,
|
||||||
skApiClient apiClient, GeoIpService geoIpService) {
|
skApiClient apiClient, GeoIpService geoIpService, UserDao userRepo) {
|
||||||
this.presupuestadorItems = presupuestadorItems;
|
this.presupuestadorItems = presupuestadorItems;
|
||||||
this.presupuestoFormatter = presupuestoFormatter;
|
this.presupuestoFormatter = presupuestoFormatter;
|
||||||
this.apiClient = apiClient;
|
this.apiClient = apiClient;
|
||||||
this.geoIpService = geoIpService;
|
this.geoIpService = geoIpService;
|
||||||
|
this.userRepo = userRepo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean validateDatosGenerales(int[] tiradas) {
|
public boolean validateDatosGenerales(int[] tiradas) {
|
||||||
@ -439,9 +444,8 @@ public class PresupuestoService {
|
|||||||
presupuesto.getSelectedTirada() != null ? presupuesto.getSelectedTirada() : tirada_min);
|
presupuesto.getSelectedTirada() != null ? presupuesto.getSelectedTirada() : tirada_min);
|
||||||
Double precio_retractilado = apiClient.getRetractilado(requestBody);
|
Double precio_retractilado = apiClient.getRetractilado(requestBody);
|
||||||
return precio_retractilado != null
|
return precio_retractilado != null
|
||||||
? NumberFormat.getNumberInstance(locale)
|
? String.valueOf(Math.round(precio_retractilado * 100.0) / 100.0)
|
||||||
.format(Math.round(precio_retractilado * 100.0) / 100.0)
|
: "0.00";
|
||||||
: "0,00";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, Object> obtenerServiciosExtras(Presupuesto presupuesto, Locale locale) {
|
public Map<String, Object> obtenerServiciosExtras(Presupuesto presupuesto, Locale locale) {
|
||||||
@ -517,7 +521,7 @@ public class PresupuestoService {
|
|||||||
put("priceUnit", "");
|
put("priceUnit", "");
|
||||||
} else {
|
} else {
|
||||||
put("price",
|
put("price",
|
||||||
NumberFormat.getNumberInstance(locale).format(Math.round(price_prototipo * 100.0) / 100.0));
|
String.valueOf(Math.round(price_prototipo * 100.0) / 100.0));
|
||||||
put("priceUnit", messageSource.getMessage("app.currency-symbol", null, locale));
|
put("priceUnit", messageSource.getMessage("app.currency-symbol", null, locale));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -723,7 +727,8 @@ public class PresupuestoService {
|
|||||||
* Calcula el resumen (SIN persistir cambios de estado).
|
* Calcula el resumen (SIN persistir cambios de estado).
|
||||||
* Mantiene firma para no romper llamadas existentes.
|
* Mantiene firma para no romper llamadas existentes.
|
||||||
*/
|
*/
|
||||||
public Map<String, Object> getResumen(Presupuesto presupuesto, List<Map<String, Object>> servicios, Locale locale) {
|
public Map<String, Object> getTextosResumen(Presupuesto presupuesto, List<Map<String, Object>> servicios,
|
||||||
|
Locale locale) {
|
||||||
Map<String, Object> resumen = new HashMap<>();
|
Map<String, Object> resumen = new HashMap<>();
|
||||||
resumen.put("titulo", presupuesto.getTitulo());
|
resumen.put("titulo", presupuesto.getTitulo());
|
||||||
|
|
||||||
@ -798,25 +803,88 @@ public class PresupuestoService {
|
|||||||
* Se invoca al entrar en la pestaña "Resumen" del presupuestador público.
|
* Se invoca al entrar en la pestaña "Resumen" del presupuestador público.
|
||||||
*/
|
*/
|
||||||
// PresupuestoService.java
|
// PresupuestoService.java
|
||||||
|
public Map<String, Object> getResumen(
|
||||||
@Transactional
|
|
||||||
public Map<String, Object> getResumenPublico(
|
|
||||||
Presupuesto presupuesto,
|
Presupuesto presupuesto,
|
||||||
List<Map<String, Object>> servicios,
|
List<Map<String, Object>> servicios,
|
||||||
|
Boolean save,
|
||||||
|
String mode,
|
||||||
Locale locale,
|
Locale locale,
|
||||||
String sessionId,
|
String sessionId,
|
||||||
String ip) {
|
String ip) {
|
||||||
|
|
||||||
// 1) Calcula el resumen (como ya haces)
|
// 1) Calcula el resumen (como ya haces)
|
||||||
Map<String, Object> resumen = getResumen(presupuesto, servicios, locale);
|
Map<String, Object> resumen = getTextosResumen(presupuesto, servicios, locale);
|
||||||
if (resumen.containsKey("error"))
|
if (resumen.containsKey("error"))
|
||||||
return resumen;
|
return resumen;
|
||||||
|
|
||||||
// 2) Totales a partir del resumen
|
presupuesto = generateTotalizadores(presupuesto, servicios, resumen, locale);
|
||||||
// - precio_unitario: primer precio devuelto por la API
|
|
||||||
// - cantidad: selected_tirada
|
// 3) Enriquecer el Presupuesto a persistir
|
||||||
// - precio_total_tirada
|
presupuesto.setEstado(Presupuesto.Estado.borrador);
|
||||||
// - servicios_total (si hay)
|
if (mode.equals("public")) {
|
||||||
|
presupuesto.setOrigen(Presupuesto.Origen.publico);
|
||||||
|
presupuesto.setSessionId(sessionId);
|
||||||
|
// IP: guarda hash y trunc (si tienes campos). Si no, guarda tal cual en
|
||||||
|
// ip_trunc/ip_hash según tu modelo.
|
||||||
|
String ipTrunc = anonymizeIp(ip);
|
||||||
|
presupuesto.setIpTrunc(ipTrunc);
|
||||||
|
presupuesto.setIpHash(Integer.toHexString(ip.hashCode()));
|
||||||
|
|
||||||
|
// ubicación (si tienes un servicio GeoIP disponible; si no, omite estas tres
|
||||||
|
// líneas)
|
||||||
|
try {
|
||||||
|
GeoIpService.GeoData geo = geoIpService.lookup(ip).orElse(null);
|
||||||
|
presupuesto.setPais(geo.getPais());
|
||||||
|
presupuesto.setRegion(geo.getRegion());
|
||||||
|
presupuesto.setCiudad(geo.getCiudad());
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
presupuesto.setOrigen(Presupuesto.Origen.privado);
|
||||||
|
|
||||||
|
// 4) UPSERT: si viene id -> actualiza; si no, reusa el último borrador de la
|
||||||
|
// sesión
|
||||||
|
Presupuesto entidad;
|
||||||
|
if (presupuesto.getId() != null) {
|
||||||
|
entidad = presupuestoRepository.findById(presupuesto.getId()).orElse(presupuesto);
|
||||||
|
} else {
|
||||||
|
entidad = presupuestoRepository
|
||||||
|
.findTopBySessionIdAndEstadoOrderByCreatedAtDesc(sessionId, Presupuesto.Estado.borrador)
|
||||||
|
.orElse(presupuesto);
|
||||||
|
// Si se reutiliza un borrador existente, copia el ID a nuestro objeto para
|
||||||
|
// hacer merge
|
||||||
|
presupuesto.setId(entidad.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5) Guardar/actualizar
|
||||||
|
entidad = mergePresupuesto(entidad, presupuesto);
|
||||||
|
|
||||||
|
if (save != null && save) {
|
||||||
|
// Si NO es para guardar (solo calcular resumen), devolver sin persistir
|
||||||
|
this.guardarPresupuesto(presupuesto);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opcional: devolver el id guardado al frontend para que lo envíe en llamadas
|
||||||
|
// siguientes
|
||||||
|
resumen.put("presupuesto_id", entidad.getId());
|
||||||
|
resumen.put("precio_unitario", presupuesto.getPrecioUnitario());
|
||||||
|
resumen.put("precio_total_tirada", presupuesto.getPrecioTotalTirada());
|
||||||
|
resumen.put("servicios_total", presupuesto.getServiciosTotal());
|
||||||
|
resumen.put("base_imponible", presupuesto.getBaseImponible());
|
||||||
|
resumen.put("iva_tipo", presupuesto.getIvaTipo());
|
||||||
|
resumen.put("iva_importe", presupuesto.getIvaImporte());
|
||||||
|
resumen.put("total_con_iva", presupuesto.getTotalConIva());
|
||||||
|
|
||||||
|
return resumen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Presupuesto generateTotalizadores(
|
||||||
|
Presupuesto presupuesto,
|
||||||
|
List<Map<String, Object>> servicios,
|
||||||
|
Map<String, Object> resumen,
|
||||||
|
Locale locale) {
|
||||||
|
|
||||||
|
// Genera los totalizadores (precio unitario, total tirada, etc.) sin guardar
|
||||||
double precioUnit = 0.0;
|
double precioUnit = 0.0;
|
||||||
int cantidad = presupuesto.getSelectedTirada() != null ? presupuesto.getSelectedTirada() : 0;
|
int cantidad = presupuesto.getSelectedTirada() != null ? presupuesto.getSelectedTirada() : 0;
|
||||||
try {
|
try {
|
||||||
@ -871,27 +939,6 @@ public class PresupuestoService {
|
|||||||
RoundingMode.HALF_UP);
|
RoundingMode.HALF_UP);
|
||||||
BigDecimal totalConIva = baseImponible.add(ivaImporte);
|
BigDecimal totalConIva = baseImponible.add(ivaImporte);
|
||||||
|
|
||||||
// 3) Enriquecer el Presupuesto a persistir
|
|
||||||
presupuesto.setEstado(Presupuesto.Estado.borrador);
|
|
||||||
presupuesto.setOrigen(Presupuesto.Origen.publico);
|
|
||||||
presupuesto.setSessionId(sessionId);
|
|
||||||
|
|
||||||
// IP: guarda hash y trunc (si tienes campos). Si no, guarda tal cual en
|
|
||||||
// ip_trunc/ip_hash según tu modelo.
|
|
||||||
String ipTrunc = anonymizeIp(ip);
|
|
||||||
presupuesto.setIpTrunc(ipTrunc);
|
|
||||||
presupuesto.setIpHash(Integer.toHexString(ip.hashCode()));
|
|
||||||
|
|
||||||
// ubicación (si tienes un servicio GeoIP disponible; si no, omite estas tres
|
|
||||||
// líneas)
|
|
||||||
try {
|
|
||||||
GeoIpService.GeoData geo = geoIpService.lookup(ip).orElse(null);
|
|
||||||
presupuesto.setPais(geo.getPais());
|
|
||||||
presupuesto.setRegion(geo.getRegion());
|
|
||||||
presupuesto.setCiudad(geo.getCiudad());
|
|
||||||
} catch (Exception ignore) {
|
|
||||||
}
|
|
||||||
|
|
||||||
// precios y totales
|
// precios y totales
|
||||||
presupuesto.setPrecioUnitario(BigDecimal.valueOf(precioUnit).setScale(6, RoundingMode.HALF_UP));
|
presupuesto.setPrecioUnitario(BigDecimal.valueOf(precioUnit).setScale(6, RoundingMode.HALF_UP));
|
||||||
presupuesto.setPrecioTotalTirada(precioTotalTirada);
|
presupuesto.setPrecioTotalTirada(precioTotalTirada);
|
||||||
@ -901,46 +948,33 @@ public class PresupuestoService {
|
|||||||
presupuesto.setIvaImporte(ivaImporte);
|
presupuesto.setIvaImporte(ivaImporte);
|
||||||
presupuesto.setTotalConIva(totalConIva);
|
presupuesto.setTotalConIva(totalConIva);
|
||||||
|
|
||||||
// 4) UPSERT: si viene id -> actualiza; si no, reusa el último borrador de la
|
return presupuesto;
|
||||||
// sesión
|
}
|
||||||
Presupuesto entidad;
|
|
||||||
if (presupuesto.getId() != null) {
|
@Transactional
|
||||||
entidad = presupuestoRepository.findById(presupuesto.getId()).orElse(presupuesto);
|
public HashMap<String, Object> guardarPresupuesto(Presupuesto presupuesto) {
|
||||||
} else {
|
|
||||||
entidad = presupuestoRepository
|
HashMap<String, Object> result = new HashMap<>();
|
||||||
.findTopBySessionIdAndEstadoOrderByCreatedAtDesc(sessionId, Presupuesto.Estado.borrador)
|
try {
|
||||||
.orElse(presupuesto);
|
|
||||||
// Si se reutiliza un borrador existente, copia el ID a nuestro objeto para
|
Presupuesto p = presupuestoRepository.saveAndFlush(presupuesto);
|
||||||
// hacer merge
|
|
||||||
presupuesto.setId(entidad.getId());
|
result.put("success", true);
|
||||||
|
result.put("presupuesto_id", p.getId());
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.out.println("Error guardando presupuesto: " + e.getMessage());
|
||||||
|
result.put("success", false);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5) Guardar/actualizar
|
|
||||||
entidad = mergePresupuesto(entidad, presupuesto);
|
|
||||||
presupuestoRepository.saveAndFlush(entidad);
|
|
||||||
|
|
||||||
// Opcional: devolver el id guardado al frontend para que lo envíe en llamadas
|
|
||||||
// siguientes
|
|
||||||
resumen.put("presupuesto_id", entidad.getId());
|
|
||||||
resumen.put("precio_unitario", presupuesto.getPrecioUnitario());
|
|
||||||
resumen.put("precio_total_tirada", presupuesto.getPrecioTotalTirada());
|
|
||||||
resumen.put("servicios_total", presupuesto.getServiciosTotal());
|
|
||||||
resumen.put("base_imponible", presupuesto.getBaseImponible());
|
|
||||||
resumen.put("iva_tipo", presupuesto.getIvaTipo());
|
|
||||||
resumen.put("iva_importe", presupuesto.getIvaImporte());
|
|
||||||
resumen.put("total_con_iva", presupuesto.getTotalConIva());
|
|
||||||
|
|
||||||
return resumen;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PRIVADO (futuro botón "Guardar"): persiste el presupuesto como borrador.
|
* PRIVADO (futuro botón "Guardar"): persiste el presupuesto como borrador.
|
||||||
*/
|
*/
|
||||||
@Transactional
|
|
||||||
public Presupuesto guardarPrivado(Presupuesto presupuesto) {
|
|
||||||
presupuesto.setEstado(Presupuesto.Estado.borrador);
|
|
||||||
return presupuestoRepository.saveAndFlush(presupuesto);
|
|
||||||
}
|
|
||||||
|
|
||||||
public HashMap<String, Object> calcularPresupuesto(Presupuesto presupuesto, Locale locale) {
|
public HashMap<String, Object> calcularPresupuesto(Presupuesto presupuesto, Locale locale) {
|
||||||
HashMap<String, Object> price = new HashMap<>();
|
HashMap<String, Object> price = new HashMap<>();
|
||||||
@ -957,7 +991,6 @@ public class PresupuestoService {
|
|||||||
return price;
|
return price;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Boolean canAccessPresupuesto(Presupuesto presupuesto, Authentication authentication) {
|
public Boolean canAccessPresupuesto(Presupuesto presupuesto, Authentication authentication) {
|
||||||
|
|
||||||
boolean isUser = authentication.getAuthorities().stream()
|
boolean isUser = authentication.getAuthorities().stream()
|
||||||
@ -973,7 +1006,6 @@ public class PresupuestoService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// =======================================================================
|
// =======================================================================
|
||||||
// Métodos privados
|
// Métodos privados
|
||||||
// =======================================================================
|
// =======================================================================
|
||||||
@ -1095,5 +1127,4 @@ public class PresupuestoService {
|
|||||||
return ip;
|
return ip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,14 @@
|
|||||||
package com.imprimelibros.erp.presupuesto.validation;
|
package com.imprimelibros.erp.presupuesto.validation;
|
||||||
|
|
||||||
|
import jakarta.validation.GroupSequence;
|
||||||
|
|
||||||
public class PresupuestoValidationGroups {
|
public class PresupuestoValidationGroups {
|
||||||
|
|
||||||
public interface DatosGenerales {}
|
public interface DatosGenerales {}
|
||||||
public interface Interior {}
|
public interface Interior {}
|
||||||
public interface Cubierta {}
|
public interface Cubierta {}
|
||||||
|
|
||||||
|
@GroupSequence({DatosGenerales.class, Interior.class, Cubierta.class})
|
||||||
|
public interface All {}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -255,7 +255,6 @@ public class User {
|
|||||||
", fullName='" + fullName + '\'' +
|
", fullName='" + fullName + '\'' +
|
||||||
", userName='" + userName + '\'' +
|
", userName='" + userName + '\'' +
|
||||||
", enabled=" + enabled +
|
", enabled=" + enabled +
|
||||||
", roles=" + getRoles() +
|
|
||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,8 +8,10 @@ import jakarta.servlet.http.HttpServletRequest;
|
|||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import org.springframework.context.MessageSource;
|
import org.springframework.context.MessageSource;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
import org.springframework.data.jpa.domain.Specification;
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@ -25,6 +27,9 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
|||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
|
||||||
import com.imprimelibros.erp.datatables.DataTablesRequest;
|
import com.imprimelibros.erp.datatables.DataTablesRequest;
|
||||||
import com.imprimelibros.erp.datatables.DataTablesParser;
|
import com.imprimelibros.erp.datatables.DataTablesParser;
|
||||||
import com.imprimelibros.erp.config.Sanitizer;
|
import com.imprimelibros.erp.config.Sanitizer;
|
||||||
@ -34,6 +39,7 @@ import java.util.Map;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
@ -52,6 +58,7 @@ public class UserController {
|
|||||||
private Sanitizer sanitizer;
|
private Sanitizer sanitizer;
|
||||||
private PasswordEncoder passwordEncoder;
|
private PasswordEncoder passwordEncoder;
|
||||||
private TranslationService translationService;
|
private TranslationService translationService;
|
||||||
|
private UserService userService;
|
||||||
|
|
||||||
public UserController(UserDao repo, UserService userService, MessageSource messageSource, Sanitizer sanitizer,
|
public UserController(UserDao repo, UserService userService, MessageSource messageSource, Sanitizer sanitizer,
|
||||||
PasswordEncoder passwordEncoder, RoleDao roleRepo, TranslationService translationService) {
|
PasswordEncoder passwordEncoder, RoleDao roleRepo, TranslationService translationService) {
|
||||||
@ -61,6 +68,7 @@ public class UserController {
|
|||||||
this.roleRepo = roleRepo;
|
this.roleRepo = roleRepo;
|
||||||
this.passwordEncoder = passwordEncoder;
|
this.passwordEncoder = passwordEncoder;
|
||||||
this.translationService = translationService;
|
this.translationService = translationService;
|
||||||
|
this.userService = userService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@ -346,4 +354,35 @@ public class UserController {
|
|||||||
}).orElseGet(() -> ResponseEntity.status(HttpStatus.NOT_FOUND)
|
}).orElseGet(() -> ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||||
.body(Map.of("message", messageSource.getMessage("usuarios.error.not-found", null, locale))));
|
.body(Map.of("message", messageSource.getMessage("usuarios.error.not-found", null, locale))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ResponseBody
|
||||||
|
@GetMapping(value = "api/get-users", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
public Map<String, Object> getUsers(
|
||||||
|
@RequestParam(required = false) String role, // puede venir ausente
|
||||||
|
@RequestParam(required = false) String q,
|
||||||
|
@RequestParam(defaultValue = "1") int page,
|
||||||
|
@RequestParam(defaultValue = "10") int size) {
|
||||||
|
|
||||||
|
Pageable pageable = PageRequest.of(Math.max(0, page - 1), size);
|
||||||
|
|
||||||
|
Page<User> users = userService.findByRoleAndSearch(role, q, pageable);
|
||||||
|
|
||||||
|
boolean more = users.hasNext();
|
||||||
|
|
||||||
|
List<Map<String, Object>> results = users.getContent().stream()
|
||||||
|
.map(u -> {
|
||||||
|
Map<String, Object> m = new HashMap<>();
|
||||||
|
m.put("id", u.getId());
|
||||||
|
m.put("text", (u.getFullName() != null && !u.getFullName().isBlank())
|
||||||
|
? u.getFullName()
|
||||||
|
: u.getUserName());
|
||||||
|
return m;
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
return Map.of(
|
||||||
|
"results", results,
|
||||||
|
"pagination", Map.of("more", more));
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,39 +19,60 @@ import org.springframework.lang.Nullable;
|
|||||||
@Repository
|
@Repository
|
||||||
public interface UserDao extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
|
public interface UserDao extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
|
||||||
|
|
||||||
// Aplicamos EntityGraph a la versión con Specification+Pageable
|
// Aplicamos EntityGraph a la versión con Specification+Pageable
|
||||||
@Override
|
@Override
|
||||||
@EntityGraph(attributePaths = { "rolesLink", "rolesLink.role" })
|
@EntityGraph(attributePaths = { "rolesLink", "rolesLink.role" })
|
||||||
@NonNull
|
@NonNull
|
||||||
Page<User> findAll(@Nullable Specification<User> spec, @NonNull Pageable pageable);
|
Page<User> findAll(@Nullable Specification<User> spec, @NonNull Pageable pageable);
|
||||||
|
|
||||||
Optional<User> findByUserNameIgnoreCase(String userName);
|
Optional<User> findByUserNameIgnoreCase(String userName);
|
||||||
|
|
||||||
boolean existsByUserNameIgnoreCase(String userName);
|
boolean existsByUserNameIgnoreCase(String userName);
|
||||||
|
|
||||||
// Para comprobar si existe al hacer signup
|
// Para comprobar si existe al hacer signup
|
||||||
@Query(value = """
|
@Query(value = """
|
||||||
SELECT id, deleted, enabled
|
SELECT id, deleted, enabled
|
||||||
FROM users
|
FROM users
|
||||||
WHERE LOWER(username) = LOWER(:userName)
|
WHERE LOWER(username) = LOWER(:userName)
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
""", nativeQuery = true)
|
""", nativeQuery = true)
|
||||||
Optional<UserLite> findLiteByUserNameIgnoreCase(@Param("userName") String userName);
|
Optional<UserLite> findLiteByUserNameIgnoreCase(@Param("userName") String userName);
|
||||||
|
|
||||||
boolean existsByUserNameIgnoreCaseAndIdNot(String userName, Long id);
|
boolean existsByUserNameIgnoreCaseAndIdNot(String userName, Long id);
|
||||||
|
|
||||||
// Nuevo: para login/negocio "activo"
|
// Nuevo: para login/negocio "activo"
|
||||||
@EntityGraph(attributePaths = { "rolesLink", "rolesLink.role" })
|
@EntityGraph(attributePaths = { "rolesLink", "rolesLink.role" })
|
||||||
Optional<User> findByUserNameIgnoreCaseAndEnabledTrueAndDeletedFalse(String userName);
|
Optional<User> findByUserNameIgnoreCaseAndEnabledTrueAndDeletedFalse(String userName);
|
||||||
|
|
||||||
// Para poder restaurar, necesitas leer ignorando @Where (native):
|
// Para poder restaurar, necesitas leer ignorando @Where (native):
|
||||||
@Query(value = "SELECT * FROM users WHERE id = :id", nativeQuery = true)
|
@Query(value = "SELECT * FROM users WHERE id = :id", nativeQuery = true)
|
||||||
Optional<User> findByIdIncludingDeleted(@Param("id") Long id);
|
Optional<User> findByIdIncludingDeleted(@Param("id") Long id);
|
||||||
|
|
||||||
@Query(value = "SELECT * FROM users WHERE deleted = TRUE", nativeQuery = true)
|
@Query(value = "SELECT * FROM users WHERE deleted = TRUE", nativeQuery = true)
|
||||||
List<User> findAllDeleted();
|
List<User> findAllDeleted();
|
||||||
|
|
||||||
@Query("select u.id from User u where lower(u.userName) = lower(:userName)")
|
@Query("select u.id from User u where lower(u.userName) = lower(:userName)")
|
||||||
Optional<Long> findIdByUserNameIgnoreCase(@Param("userName") String userName);
|
Optional<Long> findIdByUserNameIgnoreCase(@Param("userName") String userName);
|
||||||
|
|
||||||
|
@Query(value = """
|
||||||
|
SELECT DISTINCT u
|
||||||
|
FROM User u
|
||||||
|
JOIN u.rolesLink rl
|
||||||
|
JOIN rl.role r
|
||||||
|
WHERE (:role IS NULL OR r.name = :role)
|
||||||
|
AND (:q IS NULL OR LOWER(u.fullName) LIKE LOWER(CONCAT('%', :q, '%'))
|
||||||
|
OR LOWER(u.userName) LIKE LOWER(CONCAT('%', :q, '%')))
|
||||||
|
""", countQuery = """
|
||||||
|
SELECT COUNT(DISTINCT u.id)
|
||||||
|
FROM User u
|
||||||
|
JOIN u.rolesLink rl
|
||||||
|
JOIN rl.role r
|
||||||
|
WHERE (:role IS NULL OR r.name = :role)
|
||||||
|
AND (:q IS NULL OR LOWER(u.fullName) LIKE LOWER(CONCAT('%', :q, '%'))
|
||||||
|
OR LOWER(u.userName) LIKE LOWER(CONCAT('%', :q, '%')))
|
||||||
|
""")
|
||||||
|
Page<User> searchUsers(@Param("role") String role,
|
||||||
|
@Param("q") String q,
|
||||||
|
Pageable pageable);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,19 @@
|
|||||||
package com.imprimelibros.erp.users;
|
package com.imprimelibros.erp.users;
|
||||||
|
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
|
||||||
public interface UserService extends UserDetailsService {
|
public interface UserService extends UserDetailsService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Busca usuarios por rol y texto libre (en username, email o fullName),
|
||||||
|
* con paginación.
|
||||||
|
*
|
||||||
|
* @param role nombre del rol (ej. "ROL_USER")
|
||||||
|
* @param query texto de búsqueda (puede ser null o vacío)
|
||||||
|
* @param pageable paginación
|
||||||
|
* @return página de usuarios
|
||||||
|
*/
|
||||||
|
Page<User> findByRoleAndSearch(String role, String query, Pageable pageable);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,8 @@ package com.imprimelibros.erp.users;
|
|||||||
|
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@ -13,10 +15,19 @@ public class UserServiceImpl implements UserService {
|
|||||||
this.userDao = userDao;
|
this.userDao = userDao;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserDetails loadUserByUsername(String username) {
|
public UserDetails loadUserByUsername(String username) {
|
||||||
User user = userDao.findByUserNameIgnoreCaseAndEnabledTrueAndDeletedFalse(username)
|
User user = userDao.findByUserNameIgnoreCaseAndEnabledTrueAndDeletedFalse(username)
|
||||||
.orElseThrow(() -> new UsernameNotFoundException("No existe usuario activo: " + username));
|
.orElseThrow(() -> new UsernameNotFoundException("No existe usuario activo: " + username));
|
||||||
return new UserDetailsImpl(user);
|
return new UserDetailsImpl(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Búsqueda para Select2 =====
|
||||||
|
@Override
|
||||||
|
public Page<User> findByRoleAndSearch(String role, String query, Pageable pageable) {
|
||||||
|
|
||||||
|
if (query == null || query.isBlank()) query = null;
|
||||||
|
return userDao.searchUsers(role, query, pageable);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,11 +33,9 @@ safekat.api.password=Safekat2024
|
|||||||
#
|
#
|
||||||
# Debug JPA / Hibernate
|
# Debug JPA / Hibernate
|
||||||
#
|
#
|
||||||
#spring.jpa.show-sql=true
|
logging.level.org.hibernate.SQL=DEBUG
|
||||||
#logging.level.org.hibernate.SQL=DEBUG
|
logging.level.org.hibernate.orm.jdbc.bind=TRACE
|
||||||
#logging.level.org.hibernate.orm.jdbc.bind=TRACE
|
spring.jpa.properties.hibernate.format_sql=true
|
||||||
#spring.jpa.properties.hibernate.format_sql=true
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Resource chain
|
# Resource chain
|
||||||
|
|||||||
@ -10,6 +10,7 @@ presupuesto.resumen=Resumen
|
|||||||
presupuesto.add-to-presupuesto=Añadir al presupuesto
|
presupuesto.add-to-presupuesto=Añadir al presupuesto
|
||||||
presupuesto.calcular=Calcular
|
presupuesto.calcular=Calcular
|
||||||
presupuesto.add=Añadir presupuesto
|
presupuesto.add=Añadir presupuesto
|
||||||
|
presupuesto.guardar=Guardar
|
||||||
|
|
||||||
presupuesto.nav.presupuestos-cliente=Presupuestos cliente
|
presupuesto.nav.presupuestos-cliente=Presupuestos cliente
|
||||||
presupuesto.nav.presupuestos-anonimos=Presupuestos anónimos
|
presupuesto.nav.presupuestos-anonimos=Presupuestos anónimos
|
||||||
@ -278,6 +279,19 @@ presupuesto.error.delete-permission-denied=No se puede eliminar: permiso denegad
|
|||||||
presupuesto.error.delete-not-found=No se puede eliminar: presupuesto no encontrado.
|
presupuesto.error.delete-not-found=No se puede eliminar: presupuesto no encontrado.
|
||||||
presupuesto.error.delete-not-draft=Solo se pueden eliminar presupuestos en estado Borrador.
|
presupuesto.error.delete-not-draft=Solo se pueden eliminar presupuestos en estado Borrador.
|
||||||
|
|
||||||
|
# Añadir presupuesto
|
||||||
|
presupuesto.add.tipo=Tipo de presupuesto
|
||||||
|
presupuesto.add.anonimo=Anónimo
|
||||||
|
presupuesto.add.cliente=De cliente
|
||||||
|
presupuesto.add.next=Siguiente
|
||||||
|
presupuesto.add.cancel=Cancelar
|
||||||
|
presupuesto.add.select-client=Seleccione cliente
|
||||||
|
presupuesto.add.error.options=Debe seleccionar una opción
|
||||||
|
presupuesto.add.error.options-client=Debe seleccionar un cliente
|
||||||
|
presupuesto.error.save-internal-error=No se puede guardar: error interno.
|
||||||
|
presupuesto.exito.guardado=Presupuesto guardado con éxito.
|
||||||
|
presupuesto.exito.guardado-actualizado=Presupuesto actualizado con éxito.
|
||||||
|
|
||||||
# Errores
|
# Errores
|
||||||
presupuesto.errores-title=Corrija los siguientes errores:
|
presupuesto.errores-title=Corrija los siguientes errores:
|
||||||
presupuesto.errores.titulo=El título es obligatorio
|
presupuesto.errores.titulo=El título es obligatorio
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
import PresupuestoWizard from './wizard.js';
|
||||||
|
|
||||||
|
const app = new PresupuestoWizard({
|
||||||
|
mode: 'public',
|
||||||
|
readonly: false,
|
||||||
|
canSave: true,
|
||||||
|
useSessionCache: false,
|
||||||
|
});
|
||||||
|
app.init();
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import PresupuestoWizard from './wizard.js';
|
import PresupuestoWizard from './wizard.js';
|
||||||
|
|
||||||
const app = new PresupuestoWizard({
|
const app = new PresupuestoWizard({
|
||||||
mode: 'view',
|
mode: 'public',
|
||||||
readonly: true,
|
readonly: true,
|
||||||
canSave: false,
|
canSave: false,
|
||||||
useSessionCache: false,
|
useSessionCache: false,
|
||||||
|
|||||||
@ -22,15 +22,15 @@ export default class PresupuestoWizard {
|
|||||||
titulo: '',
|
titulo: '',
|
||||||
autor: '',
|
autor: '',
|
||||||
isbn: '',
|
isbn: '',
|
||||||
tirada1: '',
|
tirada1: 10,
|
||||||
tirada2: '',
|
tirada2: '',
|
||||||
tirada3: '',
|
tirada3: '',
|
||||||
tirada4: '',
|
tirada4: '',
|
||||||
ancho: '',
|
ancho: 148,
|
||||||
alto: '',
|
alto: 218,
|
||||||
formatoPersonalizado: false,
|
formatoPersonalizado: false,
|
||||||
paginasNegro: '',
|
paginasNegro: 0,
|
||||||
paginasColor: '',
|
paginasColor: 32,
|
||||||
posicionPaginasColor: '',
|
posicionPaginasColor: '',
|
||||||
tipoEncuadernacion: 'fresado',
|
tipoEncuadernacion: 'fresado',
|
||||||
},
|
},
|
||||||
@ -99,7 +99,7 @@ export default class PresupuestoWizard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
selectedTirada: null,
|
selectedTirada: 10,
|
||||||
}
|
}
|
||||||
|
|
||||||
// pestaña datos generales
|
// pestaña datos generales
|
||||||
@ -180,12 +180,20 @@ export default class PresupuestoWizard {
|
|||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
|
|
||||||
|
$.ajaxSetup({
|
||||||
|
beforeSend: function (xhr) {
|
||||||
|
const token = document.querySelector('meta[name="_csrf"]')?.content;
|
||||||
|
const header = document.querySelector('meta[name="_csrf_header"]')?.content;
|
||||||
|
if (token && header) xhr.setRequestHeader(header, token);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const root = document.getElementById('presupuesto-app');
|
const root = document.getElementById('presupuesto-app');
|
||||||
const mode = root?.dataset.mode || 'public';
|
const mode = root?.dataset.mode || 'public';
|
||||||
const presupuestoId = root?.dataset.id || null;
|
const presupuestoId = root?.dataset.id || null;
|
||||||
|
|
||||||
let stored = null;
|
let stored = null;
|
||||||
if(this.opts.useSessionCache) {
|
if (this.opts.useSessionCache) {
|
||||||
stored = sessionStorage.getItem("formData");
|
stored = sessionStorage.getItem("formData");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -238,29 +246,75 @@ export default class PresupuestoWizard {
|
|||||||
|
|
||||||
if (this.opts.canSave) {
|
if (this.opts.canSave) {
|
||||||
$('#btn-guardar').on('click', async () => {
|
$('#btn-guardar').on('click', async () => {
|
||||||
// compón el payload con lo que ya tienes:
|
|
||||||
|
const alert = $('#form-errors');
|
||||||
|
const servicios = [];
|
||||||
|
$('.service-checkbox:checked').each(function () {
|
||||||
|
const $servicio = $(this);
|
||||||
|
servicios.push({
|
||||||
|
id: $servicio.attr('id') ?? $(`label[for="${$servicio.attr('id')}"] .service-title`).text().trim(),
|
||||||
|
label: $(`label[for="${$servicio.attr('id')}"] .service-title`).text().trim(),
|
||||||
|
units: $servicio.attr('id') === 'marcapaginas' ? self.formData.servicios.datosMarcapaginas.marcapaginas_tirada : 1,
|
||||||
|
price: $servicio.data('price') ?? $(`label[for="${$servicio.attr('id')}"] .service-price`).text().trim().replace(" " + self.divExtras.data('currency'), ''),
|
||||||
|
});
|
||||||
|
});
|
||||||
const payload = {
|
const payload = {
|
||||||
id: this.opts.presupuestoId,
|
id: this.opts.presupuestoId,
|
||||||
|
mode: this.opts.mode,
|
||||||
presupuesto: this.#getPresupuestoData(),
|
presupuesto: this.#getPresupuestoData(),
|
||||||
|
servicios: servicios,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const res = await fetch(this.opts.endpoints.save, {
|
alert.addClass('d-none').find('ul').empty();
|
||||||
method: 'POST',
|
$.ajax({
|
||||||
headers: { 'Content-Type': 'application/json' },
|
url: '/presupuesto/save',
|
||||||
body: JSON.stringify(payload)
|
type: 'POST',
|
||||||
}).then(r => r.json());
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify(payload)
|
||||||
|
}).then((data) => {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: window.languageBundle?.get('common.guardado') || 'Guardado',
|
||||||
|
timer: 1800,
|
||||||
|
buttonsStyling: false,
|
||||||
|
customClass: {
|
||||||
|
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
|
||||||
|
cancelButton: 'btn btn-light' // clases para cancelar
|
||||||
|
},
|
||||||
|
showConfirmButton: false
|
||||||
|
});
|
||||||
|
// opcional: actualizar window.PRESUPUESTO_ID/resumen con el id devuelto
|
||||||
|
if (data.id) this.opts.presupuestoId = data.id;
|
||||||
|
}).catch((xhr, status, error) => {
|
||||||
|
|
||||||
// feedback
|
const errors = xhr.responseJSON;
|
||||||
Swal.fire({
|
if (errors && typeof errors === 'object') {
|
||||||
icon: 'success',
|
if (!this.DEBUG && xhr.responseJSON.error && xhr.responseJSON.error == 'Internal Server Error') {
|
||||||
title: window.languageBundle?.get('common.guardado') || 'Guardado',
|
console.error("Error al validar los datos generales. Internal Server Error");
|
||||||
timer: 1800,
|
return;
|
||||||
showConfirmButton: false
|
}
|
||||||
|
Object.values(errors).forEach(errorMsg => {
|
||||||
|
alert.find('ul').append(`<li>${errorMsg}</li>`);
|
||||||
|
});
|
||||||
|
alert.removeClass('d-none');
|
||||||
|
} else {
|
||||||
|
alert.find('ul').append('<li>Error desconocido. Por favor, inténtelo de nuevo más tarde.</li>');
|
||||||
|
$(window).scrollTop(0);
|
||||||
|
alert.removeClass('d-none');
|
||||||
|
}
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
});
|
});
|
||||||
// opcional: actualizar window.PRESUPUESTO_ID/resumen con el id devuelto
|
|
||||||
if (res.id) window.PRESUPUESTO_ID = res.id;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Swal.fire({ icon: 'error', title: 'Error al guardar', text: e?.message || '' });
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Error al guardar',
|
||||||
|
text: e?.message || '',
|
||||||
|
buttonsStyling: false,
|
||||||
|
customClass: {
|
||||||
|
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
|
||||||
|
cancelButton: 'btn btn-light' // clases para cancelar
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -934,7 +988,12 @@ export default class PresupuestoWizard {
|
|||||||
`,
|
`,
|
||||||
confirmButtonClass: 'btn btn-primary w-xs mt-2',
|
confirmButtonClass: 'btn btn-primary w-xs mt-2',
|
||||||
showConfirmButton: false,
|
showConfirmButton: false,
|
||||||
showCloseButton: true
|
showCloseButton: true,
|
||||||
|
buttonsStyling: false,
|
||||||
|
customClass: {
|
||||||
|
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
|
||||||
|
cancelButton: 'btn btn-light' // clases para cancelar
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -946,7 +1005,12 @@ export default class PresupuestoWizard {
|
|||||||
html: window.languageBundle.get('presupuesto.impresion-cubierta-help'),
|
html: window.languageBundle.get('presupuesto.impresion-cubierta-help'),
|
||||||
confirmButtonClass: 'btn btn-primary w-xs mt-2',
|
confirmButtonClass: 'btn btn-primary w-xs mt-2',
|
||||||
showConfirmButton: false,
|
showConfirmButton: false,
|
||||||
showCloseButton: true
|
showCloseButton: true,
|
||||||
|
buttonsStyling: false,
|
||||||
|
customClass: {
|
||||||
|
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
|
||||||
|
cancelButton: 'btn btn-light' // clases para cancelar
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -955,7 +1019,7 @@ export default class PresupuestoWizard {
|
|||||||
if (!$(e.target).is(':checked'))
|
if (!$(e.target).is(':checked'))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if(this._hydrating)
|
if (this._hydrating)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
$('.tapa-dura-options').eq(0).removeClass('animate-fadeInUpBounce');
|
$('.tapa-dura-options').eq(0).removeClass('animate-fadeInUpBounce');
|
||||||
@ -1036,7 +1100,7 @@ export default class PresupuestoWizard {
|
|||||||
|
|
||||||
$(document).on('change', '.datos-cubierta', (e) => {
|
$(document).on('change', '.datos-cubierta', (e) => {
|
||||||
|
|
||||||
if(this._hydrating)
|
if (this._hydrating)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const dataToStore = this.#getCubiertaData();
|
const dataToStore = this.#getCubiertaData();
|
||||||
@ -1178,7 +1242,7 @@ export default class PresupuestoWizard {
|
|||||||
if (item.extraData["sk-id"] == this.formData.cubierta.papelCubiertaId) {
|
if (item.extraData["sk-id"] == this.formData.cubierta.papelCubiertaId) {
|
||||||
item.setSelected(true);
|
item.setSelected(true);
|
||||||
}
|
}
|
||||||
item.group='papel-cubierta';
|
item.group = 'papel-cubierta';
|
||||||
this.divPapelCubierta.append(item.render());
|
this.divPapelCubierta.append(item.render());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1493,6 +1557,8 @@ export default class PresupuestoWizard {
|
|||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
presupuesto: this.#getPresupuestoData(),
|
presupuesto: this.#getPresupuestoData(),
|
||||||
|
save: !this.opts.canSave,
|
||||||
|
mode: this.opts.mode,
|
||||||
servicios: servicios
|
servicios: servicios
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { preguntarTipoPresupuesto } from './presupuesto-utils.js';
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
// si jQuery está cargado, añade CSRF a AJAX
|
// si jQuery está cargado, añade CSRF a AJAX
|
||||||
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
|
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
|
||||||
@ -113,10 +115,34 @@
|
|||||||
// usa el mensaje del backend; fallback genérico por si no llega JSON
|
// usa el mensaje del backend; fallback genérico por si no llega JSON
|
||||||
const msg = (xhr.responseJSON && xhr.responseJSON.message)
|
const msg = (xhr.responseJSON && xhr.responseJSON.message)
|
||||||
|| 'Error al eliminar el presupuesto.';
|
|| 'Error al eliminar el presupuesto.';
|
||||||
Swal.fire({ icon: 'error', title: 'No se pudo eliminar', text: msg });
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'No se pudo eliminar',
|
||||||
|
text: msg,
|
||||||
|
buttonsStyling: false,
|
||||||
|
customClass: {
|
||||||
|
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
|
||||||
|
cancelButton: 'btn btn-light' // clases para cancelar
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#addPresupuestoButton').on('click', async function (e) {
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const res = await preguntarTipoPresupuesto();
|
||||||
|
if (!res) return;
|
||||||
|
|
||||||
|
if (res.tipo === 'anonimo') {
|
||||||
|
console.log('Crear presupuesto ANÓNIMO');
|
||||||
|
} else {
|
||||||
|
console.log('Crear presupuesto de CLIENTE:', res.clienteId, res.clienteText);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* Pregunta el tipo de presupuesto y (si aplica) selecciona cliente.
|
||||||
|
* Devuelve una promesa con { tipo: 'anonimo'|'cliente', clienteId?: string }
|
||||||
|
*/
|
||||||
|
export async function preguntarTipoPresupuesto() {
|
||||||
|
const { value: tipo } = await Swal.fire({
|
||||||
|
title: window.languageBundle.get(['presupuesto.add.tipo']) || 'Selecciona tipo de presupuesto',
|
||||||
|
input: 'radio',
|
||||||
|
inputOptions: {
|
||||||
|
anonimo: window.languageBundle.get(['presupuesto.add.anonimo']) || 'Anónimo',
|
||||||
|
cliente: window.languageBundle.get(['presupuesto.add.cliente']) || 'De cliente'
|
||||||
|
},
|
||||||
|
inputValidator: (value) => {
|
||||||
|
if (!value) {
|
||||||
|
return window.languageBundle.get(['presupuesto.add.error.options']) || 'Debes seleccionar una opción.';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButtonText: window.languageBundle.get(['presupuesto.add.next']) || 'Siguiente',
|
||||||
|
showCancelButton: true,
|
||||||
|
cancelButtonText: window.languageBundle.get(['presupuesto.add.cancel']) || 'Cancelar',
|
||||||
|
buttonsStyling: false,
|
||||||
|
customClass: {
|
||||||
|
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
|
||||||
|
cancelButton: 'btn btn-light' // clases para cancelar
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!tipo) return null; // Cancelado
|
||||||
|
|
||||||
|
if (tipo === 'anonimo') {
|
||||||
|
return { tipo };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si es de cliente, mostrar otro paso con Select2
|
||||||
|
return Swal.fire({
|
||||||
|
title: window.languageBundle.get(['presupuesto.add.select-client']) || 'Selecciona cliente',
|
||||||
|
html: `
|
||||||
|
<select id="selectCliente" class="form-select select2" style="width:100%"></select>
|
||||||
|
`,
|
||||||
|
focusConfirm: false,
|
||||||
|
buttonsStyling: false,
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: window.languageBundle.get(['presupuesto.add.next']) || 'Aceptar',
|
||||||
|
cancelButtonText: window.languageBundle.get(['presupuesto.add.cancel']) || 'Cancelar',
|
||||||
|
customClass: {
|
||||||
|
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
|
||||||
|
cancelButton: 'btn btn-light' // clases para cancelar
|
||||||
|
},
|
||||||
|
didOpen: () => {
|
||||||
|
const $select = $('#selectCliente');
|
||||||
|
// Configura Select2 (AJAX o lista estática)
|
||||||
|
$select.select2({
|
||||||
|
dropdownParent: $('.swal2-container'),
|
||||||
|
ajax: {
|
||||||
|
url: 'users/api/get-users', // ajusta a tu endpoint
|
||||||
|
dataType: 'json',
|
||||||
|
delay: 250,
|
||||||
|
data: (params) => ({ q: params.term }),
|
||||||
|
processResults: data => ({
|
||||||
|
results: data.results,
|
||||||
|
pagination: data.pagination
|
||||||
|
}),
|
||||||
|
cache: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
preConfirm: () => {
|
||||||
|
const clienteId = $('#selectCliente').val();
|
||||||
|
const clienteText = $('#selectCliente option:selected').text();
|
||||||
|
if (!clienteId) {
|
||||||
|
Swal.showValidationMessage(window.languageBundle.get(['presupuesto.add.error.select-client']) || 'Debes seleccionar un cliente.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return { tipo, clienteId, clienteText };
|
||||||
|
}
|
||||||
|
}).then((r) => (r.isConfirmed ? r.value : null));
|
||||||
|
}
|
||||||
@ -132,7 +132,16 @@ $(() => {
|
|||||||
// usa el mensaje del backend; fallback genérico por si no llega JSON
|
// usa el mensaje del backend; fallback genérico por si no llega JSON
|
||||||
const msg = (xhr.responseJSON && xhr.responseJSON.message)
|
const msg = (xhr.responseJSON && xhr.responseJSON.message)
|
||||||
|| 'Error al eliminar el usuario.';
|
|| 'Error al eliminar el usuario.';
|
||||||
Swal.fire({ icon: 'error', title: 'No se pudo eliminar', text: msg });
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'No se pudo eliminar',
|
||||||
|
text: msg,
|
||||||
|
buttonsStyling: false,
|
||||||
|
customClass: {
|
||||||
|
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
|
||||||
|
cancelButton: 'btn btn-light' // clases para cancelar
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -0,0 +1,10 @@
|
|||||||
|
<!-- templates/fragments/common.html -->
|
||||||
|
<div th:fragment="buttons(appMode)"
|
||||||
|
th:if="${appMode == 'add' or appMode == 'edit'}"
|
||||||
|
class="order-3 order-md-2 mx-md-auto d-flex">
|
||||||
|
<button id="btn-guardar" type="button"
|
||||||
|
class="btn btn-success d-flex align-items-center">
|
||||||
|
<i class="ri-save-3-line me-2"></i>
|
||||||
|
<span th:text="#{presupuesto.guardar}">Guardar</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
@ -21,7 +21,8 @@
|
|||||||
|
|
||||||
<div class="ribbon-content mt-4">
|
<div class="ribbon-content mt-4">
|
||||||
<div class="row justify-content-center imagen-container-group mt-3">
|
<div class="row justify-content-center imagen-container-group mt-3">
|
||||||
<label id="tapaBlanda" class="tapa-cubierta image-container imagen-selector" th:attr="data-summary-text=#{presupuesto.tapa-blanda}">
|
<label id="tapaBlanda" class="tapa-cubierta image-container imagen-selector"
|
||||||
|
th:attr="data-summary-text=#{presupuesto.tapa-blanda}">
|
||||||
<input type="radio" name="tipoCubierta" value="tapaBlanda" hidden>
|
<input type="radio" name="tipoCubierta" value="tapaBlanda" hidden>
|
||||||
<img class="image-presupuesto" src="/assets/images/imprimelibros/presupuestador/tapa-blanda.png"
|
<img class="image-presupuesto" src="/assets/images/imprimelibros/presupuestador/tapa-blanda.png"
|
||||||
alt="">
|
alt="">
|
||||||
@ -30,7 +31,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label id="tapaDura" class="tapa-cubierta image-container imagen-selector" th:attr="data-summary-text=#{presupuesto.tapa-dura}">
|
<label id="tapaDura" class="tapa-cubierta image-container imagen-selector"
|
||||||
|
th:attr="data-summary-text=#{presupuesto.tapa-dura}">
|
||||||
<input type="radio" name="tipoCubierta" value="tapaDura" hidden>
|
<input type="radio" name="tipoCubierta" value="tapaDura" hidden>
|
||||||
<img class="image-presupuesto"
|
<img class="image-presupuesto"
|
||||||
src="/assets/images/imprimelibros/presupuestador/tapa-dura-lomo-recto.png" alt="">
|
src="/assets/images/imprimelibros/presupuestador/tapa-dura-lomo-recto.png" alt="">
|
||||||
@ -39,7 +41,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label id="tapaDuraLomoRedondo" class="tapa-cubierta image-container imagen-selector" th:attr="data-summary-text=#{presupuesto.tapa-dura-lomo-redondo}">
|
<label id="tapaDuraLomoRedondo" class="tapa-cubierta image-container imagen-selector"
|
||||||
|
th:attr="data-summary-text=#{presupuesto.tapa-dura-lomo-redondo}">
|
||||||
<input type="radio" name="tipoCubierta" value="tapaDuraLomoRedondo" hidden>
|
<input type="radio" name="tipoCubierta" value="tapaDuraLomoRedondo" hidden>
|
||||||
<img class="image-presupuesto"
|
<img class="image-presupuesto"
|
||||||
src="/assets/images/imprimelibros/presupuestador/tapa-dura-lomo-redondo.png" alt="">
|
src="/assets/images/imprimelibros/presupuestador/tapa-dura-lomo-redondo.png" alt="">
|
||||||
@ -56,7 +59,8 @@
|
|||||||
|
|
||||||
<!-- Bloque de solapas -->
|
<!-- Bloque de solapas -->
|
||||||
<div class="d-flex gap-3">
|
<div class="d-flex gap-3">
|
||||||
<label id="sin-solapas" class="image-container imagen-selector solapas-cubierta" th:attr="data-summary-text=#{app.no}">
|
<label id="sin-solapas" class="image-container imagen-selector solapas-cubierta"
|
||||||
|
th:attr="data-summary-text=#{app.no}">
|
||||||
<input type="radio" name="solapasCubierta" value="sinSolapas" checked hidden>
|
<input type="radio" name="solapasCubierta" value="sinSolapas" checked hidden>
|
||||||
<img class="image-presupuesto"
|
<img class="image-presupuesto"
|
||||||
src="/assets/images/imprimelibros/presupuestador/sinSolapasCubierta.png" alt="">
|
src="/assets/images/imprimelibros/presupuestador/sinSolapasCubierta.png" alt="">
|
||||||
@ -65,7 +69,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label id="con-solapas" class="image-container imagen-selector solapas-cubierta" th:attr="data-summary-text=#{app.yes}">
|
<label id="con-solapas" class="image-container imagen-selector solapas-cubierta"
|
||||||
|
th:attr="data-summary-text=#{app.yes}">
|
||||||
<input type="radio" name="solapasCubierta" value="conSolapas" hidden>
|
<input type="radio" name="solapasCubierta" value="conSolapas" hidden>
|
||||||
<img class="image-presupuesto"
|
<img class="image-presupuesto"
|
||||||
src="/assets/images/imprimelibros/presupuestador/conSolapasCubierta.png" alt="">
|
src="/assets/images/imprimelibros/presupuestador/conSolapasCubierta.png" alt="">
|
||||||
@ -82,7 +87,8 @@
|
|||||||
<label for="impresion-cubierta" class="form-label"
|
<label for="impresion-cubierta" class="form-label"
|
||||||
th:text="#{presupuesto.impresion-cubierta}">Impresión de cubierta</label>
|
th:text="#{presupuesto.impresion-cubierta}">Impresión de cubierta</label>
|
||||||
<div class="input-group input-group-sm">
|
<div class="input-group input-group-sm">
|
||||||
<select class="form-select select2 datos-cubierta tapa-cubierta-summary" id="impresion-cubierta">
|
<select class="form-select select2 datos-cubierta tapa-cubierta-summary"
|
||||||
|
id="impresion-cubierta">
|
||||||
<option value="2" th:text="#{presupuesto.una-cara}">Una cara</option>
|
<option value="2" th:text="#{presupuesto.una-cara}">Una cara</option>
|
||||||
<option value="4" th:text="#{presupuesto.dos-caras}">Dos caras</option>
|
<option value="4" th:text="#{presupuesto.dos-caras}">Dos caras</option>
|
||||||
</select>
|
</select>
|
||||||
@ -276,7 +282,8 @@
|
|||||||
<div class="d-flex flex-column me-2">
|
<div class="d-flex flex-column me-2">
|
||||||
<label for="papel-sobrecubierta" class="form-label"
|
<label for="papel-sobrecubierta" class="form-label"
|
||||||
th:text="#{presupuesto.sobrecubierta-papel}">Papel</label>
|
th:text="#{presupuesto.sobrecubierta-papel}">Papel</label>
|
||||||
<select class="form-select select2 datos-cubierta sobrecubierta-item w-auto" id="papel-sobrecubierta">
|
<select class="form-select select2 datos-cubierta sobrecubierta-item w-auto"
|
||||||
|
id="papel-sobrecubierta">
|
||||||
<optgroup th:label="#{presupuesto.estucado}">
|
<optgroup th:label="#{presupuesto.estucado}">
|
||||||
<option selected value="2" data-papel-id="2" data-gramaje="170"
|
<option selected value="2" data-papel-id="2" data-gramaje="170"
|
||||||
th:text="#{presupuesto.estucado-mate} + ' 170 gr'">Estucado mate 170 gr
|
th:text="#{presupuesto.estucado-mate} + ' 170 gr'">Estucado mate 170 gr
|
||||||
@ -284,11 +291,13 @@
|
|||||||
</optgroup>
|
</optgroup>
|
||||||
<optgroup th:label="#{presupuesto.verjurado}">
|
<optgroup th:label="#{presupuesto.verjurado}">
|
||||||
<option value="1" data-papel-id="18" data-gramaje="160"
|
<option value="1" data-papel-id="18" data-gramaje="160"
|
||||||
th:text="#{presupuesto.verjurado-blanco-natural} + ' 160 gr'">Verjurado blanco natural 160 gr
|
th:text="#{presupuesto.verjurado-blanco-natural} + ' 160 gr'">Verjurado
|
||||||
|
blanco natural 160 gr
|
||||||
170 gr
|
170 gr
|
||||||
</option>
|
</option>
|
||||||
<option value="2" data-papel-id="9" data-gramaje="160"
|
<option value="2" data-papel-id="9" data-gramaje="160"
|
||||||
th:text="#{presupuesto.verjurado-ahuesado} + ' 160 gr'">Verjurado ahuesado 160 gr
|
th:text="#{presupuesto.verjurado-ahuesado} + ' 160 gr'">Verjurado ahuesado
|
||||||
|
160 gr
|
||||||
</option>
|
</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
|
|
||||||
@ -309,7 +318,8 @@
|
|||||||
<div class="d-flex flex-column me-2">
|
<div class="d-flex flex-column me-2">
|
||||||
<label for="sobrecubierta-acabado" class="form-label"
|
<label for="sobrecubierta-acabado" class="form-label"
|
||||||
th:text="#{presupuesto.acabado}">Acabado</label>
|
th:text="#{presupuesto.acabado}">Acabado</label>
|
||||||
<select class="form-select select2 datos-cubierta sobrecubierta-item w-auto" id="sobrecubierta-acabado">
|
<select class="form-select select2 datos-cubierta sobrecubierta-item w-auto"
|
||||||
|
id="sobrecubierta-acabado">
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -346,11 +356,13 @@
|
|||||||
</optgroup>
|
</optgroup>
|
||||||
<optgroup th:label="#{presupuesto.verjurado}">
|
<optgroup th:label="#{presupuesto.verjurado}">
|
||||||
<option value="1" data-papel-id="18" data-gramaje="160"
|
<option value="1" data-papel-id="18" data-gramaje="160"
|
||||||
th:text="#{presupuesto.verjurado-blanco-natural} + ' 160 gr'">Verjurado blanco natural 160 gr
|
th:text="#{presupuesto.verjurado-blanco-natural} + ' 160 gr'">Verjurado
|
||||||
|
blanco natural 160 gr
|
||||||
170 gr
|
170 gr
|
||||||
</option>
|
</option>
|
||||||
<option value="2" data-papel-id="9" data-gramaje="160"
|
<option value="2" data-papel-id="9" data-gramaje="160"
|
||||||
th:text="#{presupuesto.verjurado-ahuesado} + ' 160 gr'">Verjurado ahuesado 160 gr
|
th:text="#{presupuesto.verjurado-ahuesado} + ' 160 gr'">Verjurado ahuesado
|
||||||
|
160 gr
|
||||||
</option>
|
</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
|
|
||||||
@ -358,10 +370,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex flex-column me-2">
|
<div class="d-flex flex-column me-2">
|
||||||
<label for="alto-faja" class="form-label"
|
<label for="alto-faja" class="form-label" th:text="#{presupuesto.faja-alto}">Alto
|
||||||
th:text="#{presupuesto.faja-alto}">Alto faja</label>
|
faja</label>
|
||||||
<input type="number" class="form-control datos-cubierta faja-item w-auto"
|
<input type="number" class="form-control datos-cubierta faja-item w-auto" id="alto-faja"
|
||||||
id="alto-faja" min="50" max="120" value="80" step="1">
|
min="50" max="120" value="80" step="1">
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
<p class="mb-0">min: 50 mm</p>
|
<p class="mb-0">min: 50 mm</p>
|
||||||
<p class="alto-faja-max">max: 120 mm</p>
|
<p class="alto-faja-max">max: 120 mm</p>
|
||||||
@ -397,12 +409,15 @@
|
|||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mt-4 w-100">
|
<div class="d-flex justify-content-between align-items-center mt-4 w-100">
|
||||||
<button id="btn-prev-cubierta" type="button"
|
<button id="btn-prev-cubierta" type="button"
|
||||||
class="btn btn-light d-flex align-items-center btn-change-tab-cubierta">
|
class="btn btn-light d-flex align-items-center btn-change-tab-cubierta order-1">
|
||||||
<i class=" ri-arrow-left-circle-line label-icon align-middle fs-16 me-2"></i>
|
<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>
|
<span th:text="#{presupuesto.volver-interior}">Volver a interior</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<div th:replace="~{imprimelibros/presupuestos/presupuestador-items/_buttons :: buttons(${appMode})}"></div>
|
||||||
|
|
||||||
<button id="btn-next-cubierta" type="button"
|
<button id="btn-next-cubierta" type="button"
|
||||||
class="btn btn-secondary d-flex align-items-center btn-change-tab-cubierta">
|
class="btn btn-secondary d-flex align-items-center btn-change-tab-cubierta order-2 order-md-3">
|
||||||
<span th:text="#{presupuesto.continuar-seleccion-tirada}">Continuar a selección de tirada</span>
|
<span th:text="#{presupuesto.continuar-seleccion-tirada}">Continuar a selección de tirada</span>
|
||||||
<i class="ri-arrow-right-circle-line fs-16 ms-2"></i>
|
<i class="ri-arrow-right-circle-line fs-16 ms-2"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -252,7 +252,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex align-items-center justify-content-center gap-3 mt-3">
|
<div class="d-flex align-items-center justify-content-center gap-3 mt-3">
|
||||||
<button type="button" id="next-datos-generales" class="btn btn-secondary d-flex align-items-center ms-auto">
|
|
||||||
|
<div th:replace="~{imprimelibros/presupuestos/presupuestador-items/_buttons :: buttons(${appMode})}"></div>
|
||||||
|
|
||||||
|
<button type="button" id="next-datos-generales" class="btn btn-secondary d-flex align-items-center ms-auto order-2 order-md-3">
|
||||||
<span th:text="#{presupuesto.continuar-interior}">Continuar a diseño interior</span>
|
<span th:text="#{presupuesto.continuar-interior}">Continuar a diseño interior</span>
|
||||||
<i class="ri-arrow-right-circle-line fs-16 ms-2"></i>
|
<i class="ri-arrow-right-circle-line fs-16 ms-2"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -18,12 +18,15 @@
|
|||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mt-4 w-100">
|
<div class="d-flex justify-content-between align-items-center mt-4 w-100">
|
||||||
<button id="btn-prev-extras" type="button"
|
<button id="btn-prev-extras" type="button"
|
||||||
class="btn btn-light d-flex align-items-center btn-change-tab-extras">
|
class="btn btn-light d-flex align-items-center btn-change-tab-extras order-1">
|
||||||
<i class=" ri-arrow-left-circle-line label-icon align-middle fs-16 me-2"></i>
|
<i class=" ri-arrow-left-circle-line label-icon align-middle fs-16 me-2"></i>
|
||||||
<span th:text="#{presupuesto.volver-seleccion-tirada}">Volver a selección de tirada</span>
|
<span th:text="#{presupuesto.volver-seleccion-tirada}">Volver a selección de tirada</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<div th:replace="~{imprimelibros/presupuestos/presupuestador-items/_buttons :: buttons(${appMode})}"></div>
|
||||||
|
|
||||||
<button id="btn-next-extras" type="button"
|
<button id="btn-next-extras" type="button"
|
||||||
class="btn btn-secondary d-flex align-items-center btn-change-tab-extras">
|
class="btn btn-secondary d-flex align-items-center btn-change-tab-extras order-2 order-md-3">
|
||||||
<span><b th:text="#{presupuesto.resumen}">Resumen</b></span>
|
<span><b th:text="#{presupuesto.resumen}">Resumen</b></span>
|
||||||
<i class="ri-arrow-right-circle-line fs-16 ms-2"></i>
|
<i class="ri-arrow-right-circle-line fs-16 ms-2"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -58,11 +58,16 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mt-4 w-100">
|
<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">
|
<button id="btn-prev-interior" type="button"
|
||||||
|
class="btn btn-change-tab-interior btn-light d-flex align-items-center order-1">
|
||||||
<i class=" ri-arrow-left-circle-line label-icon align-middle fs-16 me-2"></i>
|
<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>
|
<span th:text="#{presupuesto.volver-datos-generales}">Volver a datos generales</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="btn-next-interior" type="button" class="btn btn-change-tab-interior btn-secondary d-flex align-items-center">
|
|
||||||
|
<div th:replace="~{imprimelibros/presupuestos/presupuestador-items/_buttons :: buttons(${appMode})}"></div>
|
||||||
|
|
||||||
|
<button id="btn-next-interior" type="button"
|
||||||
|
class="btn btn-change-tab-interior btn-secondary d-flex align-items-center order-2 order-md-3">
|
||||||
<span th:text="#{presupuesto.continuar-cubierta}">Continuar a diseño cubierta</span>
|
<span th:text="#{presupuesto.continuar-cubierta}">Continuar a diseño cubierta</span>
|
||||||
<i class="ri-arrow-right-circle-line fs-16 ms-2"></i>
|
<i class="ri-arrow-right-circle-line fs-16 ms-2"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -47,21 +47,24 @@
|
|||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mt-4 w-100">
|
<div class="d-flex justify-content-between align-items-center mt-4 w-100">
|
||||||
<button id="btn-prev-resumen" type="button"
|
<button id="btn-prev-resumen" type="button"
|
||||||
class="btn btn-light d-flex align-items-center btn-change-tab-resumen">
|
class="btn btn-light d-flex align-items-center btn-change-tab-resumen order-1">
|
||||||
<i class=" ri-arrow-left-circle-line label-icon align-middle fs-16 me-2"></i>
|
<i class=" ri-arrow-left-circle-line label-icon align-middle fs-16 me-2"></i>
|
||||||
<span th:text="#{presupuesto.volver-extras}">Volver a extras</span>
|
<span th:text="#{presupuesto.volver-extras}">Volver a extras</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
||||||
|
<div th:replace="~{imprimelibros/presupuestos/presupuestador-items/_buttons :: buttons(${appMode})}"></div>
|
||||||
|
|
||||||
<div th:unless="${#authorization.expression('isAuthenticated()')}">
|
<div th:unless="${#authorization.expression('isAuthenticated()')}">
|
||||||
<button id="btn-add-cart" type="button"
|
<button id="btn-add-cart" type="button"
|
||||||
class="btn btn-secondary d-flex align-items-center btn-change-tab-resumen">
|
class="btn btn-secondary d-flex align-items-center order-2 order-md-3">
|
||||||
<i class="mdi mdi-login label-icon align-middle fs-16 me-2"></i>
|
<i class="mdi mdi-login label-icon align-middle fs-16 me-2"></i>
|
||||||
<span th:text="#{presupuesto.resumen.inicie-sesion}">Inicie sesión para continuar</span>
|
<span th:text="#{presupuesto.resumen.inicie-sesion}">Inicie sesión para continuar</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div th:if="${#authorization.expression('isAuthenticated()')}">
|
<div th:if="${#authorization.expression('isAuthenticated()')}">
|
||||||
<button id="btn-add-cart" type="button"
|
<button id="btn-add-cart" type="button"
|
||||||
class="btn btn-secondary d-flex align-items-center">
|
class="btn btn-secondary d-flex align-items-center order-2 order-md-3">
|
||||||
<span th:text="#{presupuesto.resumen.agregar-cesta}">Agregar a la cesta</span>
|
<span th:text="#{presupuesto.resumen.agregar-cesta}">Agregar a la cesta</span>
|
||||||
<i class="ri-shopping-cart-2-line fs-16 ms-2"></i>
|
<i class="ri-shopping-cart-2-line fs-16 ms-2"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -28,12 +28,15 @@
|
|||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mt-4 w-100">
|
<div class="d-flex justify-content-between align-items-center mt-4 w-100">
|
||||||
<button id="btn-prev-seleccion-tirada" type="button"
|
<button id="btn-prev-seleccion-tirada" type="button"
|
||||||
class="btn btn-light d-flex align-items-center btn-change-tab-seleccion-tirada">
|
class="btn btn-light d-flex align-items-center btn-change-tab-seleccion-tirada order-1">
|
||||||
<i class=" ri-arrow-left-circle-line label-icon align-middle fs-16 me-2"></i>
|
<i class=" ri-arrow-left-circle-line label-icon align-middle fs-16 me-2"></i>
|
||||||
<span th:text="#{presupuesto.volver-cubierta}">Volver a diseño de cubierta</span>
|
<span th:text="#{presupuesto.volver-cubierta}">Volver a diseño de cubierta</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<div th:replace="~{imprimelibros/presupuestos/presupuestador-items/_buttons :: buttons(${appMode})}"></div>
|
||||||
|
|
||||||
<button id="btn-next-seleccion-tirada" type="button"
|
<button id="btn-next-seleccion-tirada" type="button"
|
||||||
class="btn btn-secondary d-flex align-items-center btn-change-tab-seleccion-tirada">
|
class="btn btn-secondary d-flex align-items-center btn-change-tab-seleccion-tirada order-2 order-md-3">
|
||||||
<span><b th:text="#{presupuesto.continuar-extras-libro}">Continuar a extras del libro</b></span>
|
<span><b th:text="#{presupuesto.continuar-extras-libro}">Continuar a extras del libro</b></span>
|
||||||
<i class="ri-arrow-right-circle-line fs-16 ms-2"></i>
|
<i class="ri-arrow-right-circle-line fs-16 ms-2"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -1,7 +1,4 @@
|
|||||||
<div id="presupuesto-app"
|
<div id="presupuesto-app" th:data-mode="${appMode} ?: 'public'" th:data-id="${id} ?: ''" th:fragment="presupuestador">
|
||||||
th:data-mode="${appMode} ?: 'public'"
|
|
||||||
th:data-id="${id} ?: ''"
|
|
||||||
th:fragment="presupuestador">
|
|
||||||
|
|
||||||
<!-- Modales-->
|
<!-- Modales-->
|
||||||
<div
|
<div
|
||||||
@ -18,6 +15,14 @@
|
|||||||
|
|
||||||
<form action="#">
|
<form action="#">
|
||||||
<input type="hidden" id="cliente_id" th:value="${cliente_id} ?: null" />
|
<input type="hidden" id="cliente_id" th:value="${cliente_id} ?: null" />
|
||||||
|
|
||||||
|
<div id="form-errors" class="alert alert-danger d-none" role="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="form-errors-alert-list">
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="step-arrow-nav mt-n3 mx-n3 mb-3">
|
<div class="step-arrow-nav mt-n3 mx-n3 mb-3">
|
||||||
|
|
||||||
<ul class="nav nav-pills nav-justified custom-nav" role="tablist">
|
<ul class="nav nav-pills nav-justified custom-nav" role="tablist">
|
||||||
@ -123,7 +128,9 @@
|
|||||||
<div class="tab-pane fade" id="pills-resumen" role="tabpanel"
|
<div class="tab-pane fade" id="pills-resumen" role="tabpanel"
|
||||||
aria-labelledby="pills-resumen-tab">
|
aria-labelledby="pills-resumen-tab">
|
||||||
|
|
||||||
<div th:include="~{imprimelibros/presupuestos/presupuestador-items/_resumen_final.html}"></div>
|
<div
|
||||||
|
th:include="~{imprimelibros/presupuestos/presupuestador-items/_resumen_final.html}">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- end tab pane -->
|
<!-- end tab pane -->
|
||||||
|
|
||||||
|
|||||||
@ -58,6 +58,9 @@
|
|||||||
<div th:if="${appMode} == 'view'">
|
<div th:if="${appMode} == 'view'">
|
||||||
<script type="module" th:src="@{/assets/js/pages/imprimelibros/presupuestador/wizard-publicos.js}"></script>
|
<script type="module" th:src="@{/assets/js/pages/imprimelibros/presupuestador/wizard-publicos.js}"></script>
|
||||||
</div>
|
</div>
|
||||||
|
<div th:if="${appMode} == 'add'">
|
||||||
|
<script type="module" th:src="@{/assets/js/pages/imprimelibros/presupuestador/wizard-publicos-add.js}"></script>
|
||||||
|
</div>
|
||||||
</th:block>
|
</th:block>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@ -100,7 +100,7 @@
|
|||||||
<script th:src="@{/assets/libs/datatables/buttons.print.min.js}"></script>
|
<script th:src="@{/assets/libs/datatables/buttons.print.min.js}"></script>
|
||||||
<script th:src="@{/assets/libs/datatables/buttons.colVis.min.js}"></script>
|
<script th:src="@{/assets/libs/datatables/buttons.colVis.min.js}"></script>
|
||||||
|
|
||||||
<script th:src="@{/assets/js/pages/imprimelibros/presupuestos/list.js}"></script>
|
<script type="module" th:src="@{/assets/js/pages/imprimelibros/presupuestos/list.js}"></script>
|
||||||
</th:block>
|
</th:block>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user