mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-01-13 00:48:49 +00:00
guardando presupuestos anonimos
This commit is contained in:
@ -46,7 +46,6 @@ public abstract class AbstractAuditedEntity {
|
||||
@Column(name = "deleted_at")
|
||||
private Instant deletedAt;
|
||||
|
||||
@LastModifiedBy
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "deleted_by")
|
||||
private User deletedBy;
|
||||
|
||||
@ -3,41 +3,45 @@ package com.imprimelibros.erp.config;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.domain.AuditorAware;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
import com.imprimelibros.erp.users.User;
|
||||
import com.imprimelibros.erp.users.UserDao;
|
||||
import com.imprimelibros.erp.users.UserDetailsImpl; // tu implementación
|
||||
|
||||
@Configuration
|
||||
@EnableJpaAuditing(auditorAwareRef = "auditorAware")
|
||||
public class JpaAuditConfig {
|
||||
|
||||
@Bean
|
||||
public AuditorAware<User> auditorAware(UserDao userDao) {
|
||||
public AuditorAware<User> auditorAware(EntityManager em) {
|
||||
return () -> {
|
||||
var auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (auth == null || !auth.isAuthenticated())
|
||||
return Optional.empty();
|
||||
var ctx = SecurityContextHolder.getContext();
|
||||
if (ctx == null) return Optional.empty();
|
||||
|
||||
var auth = ctx.getAuthentication();
|
||||
if (auth == null || !auth.isAuthenticated()) return Optional.empty();
|
||||
|
||||
Object principal = auth.getPrincipal();
|
||||
Long userId = null;
|
||||
|
||||
if (principal instanceof User u)
|
||||
return Optional.of(u);
|
||||
|
||||
if (principal instanceof UserDetails ud) {
|
||||
return userDao.findByUserNameIgnoreCase(ud.getUsername());
|
||||
// Tu UserDetailsImpl ya tiene el id
|
||||
if (principal instanceof UserDetailsImpl udi) {
|
||||
userId = udi.getId();
|
||||
}
|
||||
|
||||
if (principal instanceof String username && !"anonymousUser".equals(username)) {
|
||||
return userDao.findByUserNameIgnoreCase(username);
|
||||
// Si a veces pones el propio User como principal:
|
||||
else if (principal instanceof User u && u.getId() != null) {
|
||||
userId = u.getId();
|
||||
}
|
||||
// ⚠️ NO hagas consultas aquí (nada de userDao.findBy...).
|
||||
if (userId == null) return Optional.empty();
|
||||
|
||||
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 java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.MessageSource;
|
||||
@ -33,7 +33,7 @@ import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import jakarta.validation.Validator;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
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.validation.PresupuestoValidationGroups;
|
||||
import com.imprimelibros.erp.users.UserDao;
|
||||
import com.imprimelibros.erp.users.User;
|
||||
import com.imprimelibros.erp.users.UserDetailsImpl;
|
||||
import com.imprimelibros.erp.presupuesto.service.PresupuestoFormDataMapper;
|
||||
import com.imprimelibros.erp.presupuesto.service.PresupuestoFormDataMapper.PresupuestoFormDataDto;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
@Controller
|
||||
@ -71,6 +71,9 @@ public class PresupuestoController {
|
||||
@Autowired
|
||||
protected MessageSource messageSource;
|
||||
|
||||
@Autowired
|
||||
private Validator validator;
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
private final TranslationService translationService;
|
||||
private final PresupuestoDatatableService dtService;
|
||||
@ -472,6 +475,8 @@ public class PresupuestoController {
|
||||
HttpServletRequest request) {
|
||||
|
||||
Presupuesto p = objectMapper.convertValue(body.get("presupuesto"), Presupuesto.class);
|
||||
Boolean save = objectMapper.convertValue(body.get("save"), Boolean.class);
|
||||
String mode = objectMapper.convertValue(body.get("mode"), String.class);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> serviciosList = (List<Map<String, Object>>) body.getOrDefault("servicios", List.of());
|
||||
@ -479,7 +484,7 @@ public class PresupuestoController {
|
||||
String sessionId = request.getSession(true).getId();
|
||||
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);
|
||||
}
|
||||
@ -498,7 +503,15 @@ public class PresupuestoController {
|
||||
"app.yes",
|
||||
"app.cancelar",
|
||||
"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);
|
||||
model.addAttribute("languageBundle", translations);
|
||||
@ -557,6 +570,35 @@ public class PresupuestoController {
|
||||
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")
|
||||
public ResponseEntity<PresupuestoFormDataDto> getPresupuesto(
|
||||
@RequestParam("id") Long id, Authentication authentication) {
|
||||
@ -588,51 +630,102 @@ public class PresupuestoController {
|
||||
@Transactional
|
||||
public ResponseEntity<?> delete(@PathVariable Long id, Authentication auth, Locale locale) {
|
||||
|
||||
Presupuesto p = presupuestoRepository.findById(id)
|
||||
.orElseThrow(() -> new ResponseStatusException(
|
||||
HttpStatus.NOT_FOUND,
|
||||
messageSource.getMessage("presupuesto.error.not-found", null, locale)));
|
||||
Presupuesto p = presupuestoRepository.findById(id).orElse(null);
|
||||
if (p == null) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body(Map.of("message", messageSource.getMessage("presupuesto.error.not-found", null, locale)));
|
||||
}
|
||||
|
||||
boolean isUser = auth != null && auth.getAuthorities().stream()
|
||||
.anyMatch(a -> a.getAuthority().equals("ROLE_USER"));
|
||||
|
||||
// compara por IDs (no uses equals entre tipos distintos)
|
||||
Long ownerId = p.getUser() != null ? p.getUser().getId() : null;
|
||||
|
||||
User currentUser = null;
|
||||
Long currentUserId = null;
|
||||
if (auth != null && auth.getPrincipal() instanceof UserDetailsImpl udi) {
|
||||
currentUserId = udi.getId();
|
||||
currentUser = userRepo.findById(currentUserId).orElse(null);
|
||||
} else if (auth != null) {
|
||||
currentUserId = userRepo.findIdByUserNameIgnoreCase(auth.getName()).orElse(null); // fallback
|
||||
currentUser = userRepo.findById(currentUserId).orElse(null);
|
||||
currentUserId = userRepo.findIdByUserNameIgnoreCase(auth.getName()).orElse(null);
|
||||
}
|
||||
|
||||
boolean isOwner = ownerId != null && ownerId.equals(currentUserId);
|
||||
|
||||
if (isUser && !isOwner) {
|
||||
throw new ResponseStatusException(
|
||||
HttpStatus.FORBIDDEN,
|
||||
messageSource.getMessage("presupuesto.error.delete-permission-denied", null, locale));
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(Map.of("message",
|
||||
messageSource.getMessage("presupuesto.error.delete-permission-denied", null, locale)));
|
||||
}
|
||||
|
||||
if (p.getEstado() != null && !p.getEstado().equals(Presupuesto.Estado.borrador)) {
|
||||
throw new ResponseStatusException(
|
||||
HttpStatus.FORBIDDEN,
|
||||
messageSource.getMessage("presupuesto.error.delete-not-draft", null, locale));
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(Map.of("message",
|
||||
messageSource.getMessage("presupuesto.error.delete-not-draft", null, locale)));
|
||||
}
|
||||
|
||||
// SOFT DELETE (no uses deleteById)
|
||||
p.setDeleted(true);
|
||||
p.setDeletedAt(Instant.now());
|
||||
p.setDeletedBy(currentUser);
|
||||
try {
|
||||
p.setDeleted(true);
|
||||
p.setDeletedAt(Instant.now());
|
||||
|
||||
presupuestoRepository.save(p);
|
||||
return ResponseEntity.ok(Map.of("message",
|
||||
messageSource.getMessage("presupuesto.exito.eliminado", null, locale)));
|
||||
if (auth != null && auth.getPrincipal() instanceof UserDetailsImpl udi) {
|
||||
p.setDeletedBy(userRepo.getReferenceById(udi.getId()));
|
||||
} else if (auth != null) {
|
||||
userRepo.findByUserNameIgnoreCase(auth.getName()).ifPresent(p::setDeletedBy);
|
||||
}
|
||||
presupuestoRepository.saveAndFlush(p);
|
||||
|
||||
return ResponseEntity.ok(Map.of("message",
|
||||
messageSource.getMessage("presupuesto.exito.eliminado", null, locale)));
|
||||
|
||||
} catch (Exception ex) {
|
||||
// Devuelve SIEMPRE algo en el catch
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(Map.of("message",
|
||||
messageSource.getMessage("presupuesto.error.delete-internal-error", null, locale),
|
||||
"detail",
|
||||
ex.getClass().getSimpleName() + ": " + (ex.getMessage() != null ? ex.getMessage() : "")));
|
||||
}
|
||||
}
|
||||
|
||||
@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.stream.Collectors;
|
||||
import java.util.Locale;
|
||||
import java.text.NumberFormat;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Service;
|
||||
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.maquetacion.MaquetacionMatricesRepository;
|
||||
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;
|
||||
|
||||
@Service
|
||||
@ -65,13 +68,15 @@ public class PresupuestoService {
|
||||
private final PresupuestoFormatter presupuestoFormatter;
|
||||
private final skApiClient apiClient;
|
||||
private final GeoIpService geoIpService;
|
||||
private final UserDao userRepo;
|
||||
|
||||
public PresupuestoService(PresupuestadorItems presupuestadorItems, PresupuestoFormatter presupuestoFormatter,
|
||||
skApiClient apiClient, GeoIpService geoIpService) {
|
||||
skApiClient apiClient, GeoIpService geoIpService, UserDao userRepo) {
|
||||
this.presupuestadorItems = presupuestadorItems;
|
||||
this.presupuestoFormatter = presupuestoFormatter;
|
||||
this.apiClient = apiClient;
|
||||
this.geoIpService = geoIpService;
|
||||
this.userRepo = userRepo;
|
||||
}
|
||||
|
||||
public boolean validateDatosGenerales(int[] tiradas) {
|
||||
@ -439,9 +444,8 @@ public class PresupuestoService {
|
||||
presupuesto.getSelectedTirada() != null ? presupuesto.getSelectedTirada() : tirada_min);
|
||||
Double precio_retractilado = apiClient.getRetractilado(requestBody);
|
||||
return precio_retractilado != null
|
||||
? NumberFormat.getNumberInstance(locale)
|
||||
.format(Math.round(precio_retractilado * 100.0) / 100.0)
|
||||
: "0,00";
|
||||
? String.valueOf(Math.round(precio_retractilado * 100.0) / 100.0)
|
||||
: "0.00";
|
||||
}
|
||||
|
||||
public Map<String, Object> obtenerServiciosExtras(Presupuesto presupuesto, Locale locale) {
|
||||
@ -517,7 +521,7 @@ public class PresupuestoService {
|
||||
put("priceUnit", "");
|
||||
} else {
|
||||
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));
|
||||
}
|
||||
}
|
||||
@ -723,7 +727,8 @@ public class PresupuestoService {
|
||||
* Calcula el resumen (SIN persistir cambios de estado).
|
||||
* 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<>();
|
||||
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.
|
||||
*/
|
||||
// PresupuestoService.java
|
||||
|
||||
@Transactional
|
||||
public Map<String, Object> getResumenPublico(
|
||||
public Map<String, Object> getResumen(
|
||||
Presupuesto presupuesto,
|
||||
List<Map<String, Object>> servicios,
|
||||
Boolean save,
|
||||
String mode,
|
||||
Locale locale,
|
||||
String sessionId,
|
||||
String ip) {
|
||||
|
||||
// 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"))
|
||||
return resumen;
|
||||
|
||||
// 2) Totales a partir del resumen
|
||||
// - precio_unitario: primer precio devuelto por la API
|
||||
// - cantidad: selected_tirada
|
||||
// - precio_total_tirada
|
||||
// - servicios_total (si hay)
|
||||
presupuesto = generateTotalizadores(presupuesto, servicios, resumen, locale);
|
||||
|
||||
// 3) Enriquecer el Presupuesto a persistir
|
||||
presupuesto.setEstado(Presupuesto.Estado.borrador);
|
||||
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;
|
||||
int cantidad = presupuesto.getSelectedTirada() != null ? presupuesto.getSelectedTirada() : 0;
|
||||
try {
|
||||
@ -871,27 +939,6 @@ public class PresupuestoService {
|
||||
RoundingMode.HALF_UP);
|
||||
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
|
||||
presupuesto.setPrecioUnitario(BigDecimal.valueOf(precioUnit).setScale(6, RoundingMode.HALF_UP));
|
||||
presupuesto.setPrecioTotalTirada(precioTotalTirada);
|
||||
@ -901,46 +948,33 @@ public class PresupuestoService {
|
||||
presupuesto.setIvaImporte(ivaImporte);
|
||||
presupuesto.setTotalConIva(totalConIva);
|
||||
|
||||
// 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());
|
||||
return presupuesto;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public HashMap<String, Object> guardarPresupuesto(Presupuesto presupuesto) {
|
||||
|
||||
HashMap<String, Object> result = new HashMap<>();
|
||||
try {
|
||||
|
||||
Presupuesto p = presupuestoRepository.saveAndFlush(presupuesto);
|
||||
|
||||
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.
|
||||
*/
|
||||
@Transactional
|
||||
public Presupuesto guardarPrivado(Presupuesto presupuesto) {
|
||||
presupuesto.setEstado(Presupuesto.Estado.borrador);
|
||||
return presupuestoRepository.saveAndFlush(presupuesto);
|
||||
}
|
||||
|
||||
public HashMap<String, Object> calcularPresupuesto(Presupuesto presupuesto, Locale locale) {
|
||||
HashMap<String, Object> price = new HashMap<>();
|
||||
@ -957,9 +991,8 @@ public class PresupuestoService {
|
||||
return price;
|
||||
}
|
||||
|
||||
|
||||
public Boolean canAccessPresupuesto(Presupuesto presupuesto, Authentication authentication) {
|
||||
|
||||
|
||||
boolean isUser = authentication.getAuthorities().stream()
|
||||
.anyMatch(a -> a.getAuthority().equals("ROLE_USER"));
|
||||
|
||||
@ -972,7 +1005,6 @@ public class PresupuestoService {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// =======================================================================
|
||||
// Métodos privados
|
||||
@ -1095,5 +1127,4 @@ public class PresupuestoService {
|
||||
return ip;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
package com.imprimelibros.erp.presupuesto.validation;
|
||||
|
||||
import jakarta.validation.GroupSequence;
|
||||
|
||||
public class PresupuestoValidationGroups {
|
||||
|
||||
public interface DatosGenerales {}
|
||||
public interface Interior {}
|
||||
public interface Cubierta {}
|
||||
|
||||
@GroupSequence({DatosGenerales.class, Interior.class, Cubierta.class})
|
||||
public interface All {}
|
||||
|
||||
}
|
||||
|
||||
@ -255,7 +255,6 @@ public class User {
|
||||
", fullName='" + fullName + '\'' +
|
||||
", userName='" + userName + '\'' +
|
||||
", enabled=" + enabled +
|
||||
", roles=" + getRoles() +
|
||||
'}';
|
||||
}
|
||||
|
||||
|
||||
@ -8,8 +8,10 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@ -25,6 +27,9 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
||||
import com.imprimelibros.erp.datatables.DataTablesRequest;
|
||||
import com.imprimelibros.erp.datatables.DataTablesParser;
|
||||
import com.imprimelibros.erp.config.Sanitizer;
|
||||
@ -34,6 +39,7 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
@ -52,6 +58,7 @@ public class UserController {
|
||||
private Sanitizer sanitizer;
|
||||
private PasswordEncoder passwordEncoder;
|
||||
private TranslationService translationService;
|
||||
private UserService userService;
|
||||
|
||||
public UserController(UserDao repo, UserService userService, MessageSource messageSource, Sanitizer sanitizer,
|
||||
PasswordEncoder passwordEncoder, RoleDao roleRepo, TranslationService translationService) {
|
||||
@ -61,6 +68,7 @@ public class UserController {
|
||||
this.roleRepo = roleRepo;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.translationService = translationService;
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@ -346,4 +354,35 @@ public class UserController {
|
||||
}).orElseGet(() -> ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body(Map.of("message", messageSource.getMessage("usuarios.error.not-found", null, locale))));
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
@GetMapping(value = "api/get-users", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public Map<String, Object> getUsers(
|
||||
@RequestParam(required = false) String role, // puede venir ausente
|
||||
@RequestParam(required = false) String q,
|
||||
@RequestParam(defaultValue = "1") int page,
|
||||
@RequestParam(defaultValue = "10") int size) {
|
||||
|
||||
Pageable pageable = PageRequest.of(Math.max(0, page - 1), size);
|
||||
|
||||
Page<User> users = userService.findByRoleAndSearch(role, q, pageable);
|
||||
|
||||
boolean more = users.hasNext();
|
||||
|
||||
List<Map<String, Object>> results = users.getContent().stream()
|
||||
.map(u -> {
|
||||
Map<String, Object> m = new HashMap<>();
|
||||
m.put("id", u.getId());
|
||||
m.put("text", (u.getFullName() != null && !u.getFullName().isBlank())
|
||||
? u.getFullName()
|
||||
: u.getUserName());
|
||||
return m;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return Map.of(
|
||||
"results", results,
|
||||
"pagination", Map.of("more", more));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,39 +19,60 @@ import org.springframework.lang.Nullable;
|
||||
@Repository
|
||||
public interface UserDao extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
|
||||
|
||||
// Aplicamos EntityGraph a la versión con Specification+Pageable
|
||||
@Override
|
||||
@EntityGraph(attributePaths = { "rolesLink", "rolesLink.role" })
|
||||
@NonNull
|
||||
Page<User> findAll(@Nullable Specification<User> spec, @NonNull Pageable pageable);
|
||||
// Aplicamos EntityGraph a la versión con Specification+Pageable
|
||||
@Override
|
||||
@EntityGraph(attributePaths = { "rolesLink", "rolesLink.role" })
|
||||
@NonNull
|
||||
Page<User> findAll(@Nullable Specification<User> spec, @NonNull Pageable pageable);
|
||||
|
||||
Optional<User> findByUserNameIgnoreCase(String userName);
|
||||
Optional<User> findByUserNameIgnoreCase(String userName);
|
||||
|
||||
boolean existsByUserNameIgnoreCase(String userName);
|
||||
boolean existsByUserNameIgnoreCase(String userName);
|
||||
|
||||
// Para comprobar si existe al hacer signup
|
||||
@Query(value = """
|
||||
SELECT id, deleted, enabled
|
||||
FROM users
|
||||
WHERE LOWER(username) = LOWER(:userName)
|
||||
LIMIT 1
|
||||
""", nativeQuery = true)
|
||||
Optional<UserLite> findLiteByUserNameIgnoreCase(@Param("userName") String userName);
|
||||
// Para comprobar si existe al hacer signup
|
||||
@Query(value = """
|
||||
SELECT id, deleted, enabled
|
||||
FROM users
|
||||
WHERE LOWER(username) = LOWER(:userName)
|
||||
LIMIT 1
|
||||
""", nativeQuery = true)
|
||||
Optional<UserLite> findLiteByUserNameIgnoreCase(@Param("userName") String userName);
|
||||
|
||||
boolean existsByUserNameIgnoreCaseAndIdNot(String userName, Long id);
|
||||
boolean existsByUserNameIgnoreCaseAndIdNot(String userName, Long id);
|
||||
|
||||
// Nuevo: para login/negocio "activo"
|
||||
@EntityGraph(attributePaths = { "rolesLink", "rolesLink.role" })
|
||||
Optional<User> findByUserNameIgnoreCaseAndEnabledTrueAndDeletedFalse(String userName);
|
||||
// Nuevo: para login/negocio "activo"
|
||||
@EntityGraph(attributePaths = { "rolesLink", "rolesLink.role" })
|
||||
Optional<User> findByUserNameIgnoreCaseAndEnabledTrueAndDeletedFalse(String userName);
|
||||
|
||||
// Para poder restaurar, necesitas leer ignorando @Where (native):
|
||||
@Query(value = "SELECT * FROM users WHERE id = :id", nativeQuery = true)
|
||||
Optional<User> findByIdIncludingDeleted(@Param("id") Long id);
|
||||
// Para poder restaurar, necesitas leer ignorando @Where (native):
|
||||
@Query(value = "SELECT * FROM users WHERE id = :id", nativeQuery = true)
|
||||
Optional<User> findByIdIncludingDeleted(@Param("id") Long id);
|
||||
|
||||
@Query(value = "SELECT * FROM users WHERE deleted = TRUE", nativeQuery = true)
|
||||
List<User> findAllDeleted();
|
||||
@Query(value = "SELECT * FROM users WHERE deleted = TRUE", nativeQuery = true)
|
||||
List<User> findAllDeleted();
|
||||
|
||||
@Query("select u.id from User u where lower(u.userName) = lower(:userName)")
|
||||
Optional<Long> findIdByUserNameIgnoreCase(@Param("userName") String userName);
|
||||
@Query("select u.id from User u where lower(u.userName) = lower(:userName)")
|
||||
Optional<Long> findIdByUserNameIgnoreCase(@Param("userName") String userName);
|
||||
|
||||
@Query(value = """
|
||||
SELECT DISTINCT u
|
||||
FROM User u
|
||||
JOIN u.rolesLink rl
|
||||
JOIN rl.role r
|
||||
WHERE (:role IS NULL OR r.name = :role)
|
||||
AND (:q IS NULL OR LOWER(u.fullName) LIKE LOWER(CONCAT('%', :q, '%'))
|
||||
OR LOWER(u.userName) LIKE LOWER(CONCAT('%', :q, '%')))
|
||||
""", countQuery = """
|
||||
SELECT COUNT(DISTINCT u.id)
|
||||
FROM User u
|
||||
JOIN u.rolesLink rl
|
||||
JOIN rl.role r
|
||||
WHERE (:role IS NULL OR r.name = :role)
|
||||
AND (:q IS NULL OR LOWER(u.fullName) LIKE LOWER(CONCAT('%', :q, '%'))
|
||||
OR LOWER(u.userName) LIKE LOWER(CONCAT('%', :q, '%')))
|
||||
""")
|
||||
Page<User> searchUsers(@Param("role") String role,
|
||||
@Param("q") String q,
|
||||
Pageable pageable);
|
||||
|
||||
}
|
||||
|
||||
@ -1,7 +1,19 @@
|
||||
package com.imprimelibros.erp.users;
|
||||
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
||||
public interface UserService extends UserDetailsService {
|
||||
|
||||
|
||||
/**
|
||||
* Busca usuarios por rol y texto libre (en username, email o fullName),
|
||||
* con paginación.
|
||||
*
|
||||
* @param role nombre del rol (ej. "ROL_USER")
|
||||
* @param query texto de búsqueda (puede ser null o vacío)
|
||||
* @param pageable paginación
|
||||
* @return página de usuarios
|
||||
*/
|
||||
Page<User> findByRoleAndSearch(String role, String query, Pageable pageable);
|
||||
}
|
||||
|
||||
@ -2,6 +2,8 @@ package com.imprimelibros.erp.users;
|
||||
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@ -13,10 +15,19 @@ public class UserServiceImpl implements UserService {
|
||||
this.userDao = userDao;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) {
|
||||
User user = userDao.findByUserNameIgnoreCaseAndEnabledTrueAndDeletedFalse(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("No existe usuario activo: " + username));
|
||||
return new UserDetailsImpl(user);
|
||||
}
|
||||
|
||||
// ===== Búsqueda para Select2 =====
|
||||
@Override
|
||||
public Page<User> findByRoleAndSearch(String role, String query, Pageable pageable) {
|
||||
|
||||
if (query == null || query.isBlank()) query = null;
|
||||
return userDao.searchUsers(role, query, pageable);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user