listado de pedidos admin hecho

This commit is contained in:
2025-11-17 10:35:04 +01:00
parent 73676f60b9
commit 6ff5250d1b
12 changed files with 3307 additions and 2646 deletions

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,9 @@ import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.security.Principal; import java.security.Principal;
import java.text.NumberFormat; import java.text.NumberFormat;
import java.time.Instant;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
@ -12,6 +14,7 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import org.springframework.context.MessageSource; import org.springframework.context.MessageSource;
@ -357,4 +360,54 @@ public class Utils {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm", locale); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm", locale);
return dateTime.format(formatter); return dateTime.format(formatter);
} }
public static String formatInstant(Instant instant, Locale locale) {
if (instant == null) {
return "";
}
ZoneId zone = zoneIdForLocale(locale);
DateTimeFormatter formatter = DateTimeFormatter
.ofPattern("dd/MM/yyyy HH:mm", locale)
.withZone(zone);
return formatter.format(instant);
}
/*********************
* Metodos auxiliares
*/
private static ZoneId zoneIdForLocale(Locale locale) {
if (locale == null || locale.getCountry().isEmpty()) {
return ZoneId.of("UTC");
}
// Buscar timezones cuyo ID termine con el country code
// Ej: ES -> Europe/Madrid
String country = locale.getCountry();
Set<String> zoneIds = ZoneId.getAvailableZoneIds();
for (String id : zoneIds) {
// TimeZone# getID() no funciona por país, pero sí el prefijo + país
if (id.endsWith("/" + country) || id.contains("/" + country)) {
return ZoneId.of(id);
}
}
// fallback por regiones comunes (manual pero muy útil)
Map<String, String> fallback = Map.of(
"ES", "Europe/Madrid",
"MX", "America/Mexico_City",
"AR", "America/Argentina/Buenos_Aires",
"US", "America/New_York",
"GB", "Europe/London",
"FR", "Europe/Paris");
if (fallback.containsKey(country)) {
return ZoneId.of(fallback.get(country));
}
return ZoneId.systemDefault(); // último fallback
}
} }

View File

@ -1,11 +1,15 @@
package com.imprimelibros.erp.pedidos; package com.imprimelibros.erp.pedidos;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import com.imprimelibros.erp.common.jpa.AbstractAuditedEntity;
@Entity @Entity
@Table(name = "pedidos") @Table(name = "pedidos")
public class Pedido { public class Pedido extends AbstractAuditedEntity {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
@ -37,27 +41,8 @@ public class Pedido {
@Column(name = "proveedor_ref", length = 100) @Column(name = "proveedor_ref", length = 100)
private String proveedorRef; private String proveedorRef;
// Auditoría básica (coincidiendo con las columnas que se ven en la captura) @OneToMany(mappedBy = "pedido", cascade = CascadeType.ALL, orphanRemoval = false)
@Column(name = "created_by") private List<PedidoLinea> lineas = new ArrayList<>();
private Long createdBy;
@Column(name = "updated_by")
private Long updatedBy;
@Column(name = "deleted_by")
private Long deletedBy;
@Column(name = "deleted", nullable = false)
private boolean deleted = false;
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@Column(name = "deleted_at")
private LocalDateTime deletedAt;
// --- Getters y setters --- // --- Getters y setters ---
@ -132,60 +117,4 @@ public class Pedido {
public void setProveedorRef(String proveedorRef) { public void setProveedorRef(String proveedorRef) {
this.proveedorRef = proveedorRef; this.proveedorRef = proveedorRef;
} }
public Long getCreatedBy() {
return createdBy;
}
public void setCreatedBy(Long createdBy) {
this.createdBy = createdBy;
}
public Long getUpdatedBy() {
return updatedBy;
}
public void setUpdatedBy(Long updatedBy) {
this.updatedBy = updatedBy;
}
public Long getDeletedBy() {
return deletedBy;
}
public void setDeletedBy(Long deletedBy) {
this.deletedBy = deletedBy;
}
public boolean isDeleted() {
return deleted;
}
public void setDeleted(boolean deleted) {
this.deleted = deleted;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public LocalDateTime getDeletedAt() {
return deletedAt;
}
public void setDeletedAt(LocalDateTime deletedAt) {
this.deletedAt = deletedAt;
}
} }

View File

@ -10,21 +10,27 @@ import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
public class PedidoLinea { public class PedidoLinea {
public enum Estado { public enum Estado {
aprobado("pedido.estado.aprobado"), aprobado("pedido.estado.aprobado", 1),
maquetacion("pedido.estado.maquetacion"), maquetacion("pedido.estado.maquetacion", 2),
haciendo_ferro("pedido.estado.haciendo_ferro"), haciendo_ferro("pedido.estado.haciendo_ferro", 3),
produccion("pedido.estado.produccion"), produccion("pedido.estado.produccion", 4),
cancelado("pedido.estado.cancelado"); cancelado("pedido.estado.cancelado", 5);
private final String messageKey; private final String messageKey;
private final int priority;
Estado(String messageKey) { Estado(String messageKey, int priority) {
this.messageKey = messageKey; this.messageKey = messageKey;
this.priority = priority;
} }
public String getMessageKey() { public String getMessageKey() {
return messageKey; return messageKey;
} }
public int getPriority() {
return priority;
}
} }
@Id @Id

View File

@ -1,5 +1,6 @@
package com.imprimelibros.erp.pedidos; package com.imprimelibros.erp.pedidos;
import java.time.Instant;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -11,6 +12,7 @@ import org.springframework.transaction.annotation.Transactional;
import com.imprimelibros.erp.direcciones.Direccion; import com.imprimelibros.erp.direcciones.Direccion;
import com.imprimelibros.erp.presupuesto.PresupuestoRepository; import com.imprimelibros.erp.presupuesto.PresupuestoRepository;
import com.imprimelibros.erp.presupuesto.dto.Presupuesto; import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
import com.imprimelibros.erp.users.UserService;
import com.imprimelibros.erp.direcciones.DireccionService; import com.imprimelibros.erp.direcciones.DireccionService;
@Service @Service
@ -21,15 +23,17 @@ public class PedidoService {
private final PresupuestoRepository presupuestoRepository; private final PresupuestoRepository presupuestoRepository;
private final PedidoDireccionRepository pedidoDireccionRepository; private final PedidoDireccionRepository pedidoDireccionRepository;
private final DireccionService direccionService; private final DireccionService direccionService;
private final UserService userService;
public PedidoService(PedidoRepository pedidoRepository, PedidoLineaRepository pedidoLineaRepository, public PedidoService(PedidoRepository pedidoRepository, PedidoLineaRepository pedidoLineaRepository,
PresupuestoRepository presupuestoRepository, PedidoDireccionRepository pedidoDireccionRepository, PresupuestoRepository presupuestoRepository, PedidoDireccionRepository pedidoDireccionRepository,
DireccionService direccionService) { DireccionService direccionService, UserService userService) {
this.pedidoRepository = pedidoRepository; this.pedidoRepository = pedidoRepository;
this.pedidoLineaRepository = pedidoLineaRepository; this.pedidoLineaRepository = pedidoLineaRepository;
this.presupuestoRepository = presupuestoRepository; this.presupuestoRepository = presupuestoRepository;
this.pedidoDireccionRepository = pedidoDireccionRepository; this.pedidoDireccionRepository = pedidoDireccionRepository;
this.direccionService = direccionService; this.direccionService = direccionService;
this.userService = userService;
} }
public int getDescuentoFidelizacion() { public int getDescuentoFidelizacion() {
@ -84,11 +88,11 @@ public class PedidoService {
pedido.setProveedorRef(proveedorRef); pedido.setProveedorRef(proveedorRef);
// Auditoría mínima // Auditoría mínima
pedido.setCreatedBy(userId); pedido.setCreatedBy(userService.findById(userId));
pedido.setCreatedAt(LocalDateTime.now()); pedido.setCreatedAt(Instant.now());
pedido.setDeleted(false); pedido.setDeleted(false);
pedido.setUpdatedAt(LocalDateTime.now()); pedido.setUpdatedAt(Instant.now());
pedido.setUpdatedBy(userId); pedido.setUpdatedBy(userService.findById(userId));
// Guardamos el pedido // Guardamos el pedido
Pedido saved = pedidoRepository.save(pedido); Pedido saved = pedidoRepository.save(pedido);

View File

@ -4,10 +4,10 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import java.security.Principal; import java.security.Principal;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import org.springframework.context.MessageSource; import org.springframework.context.MessageSource;
import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.Specification;
@ -18,16 +18,12 @@ import com.imprimelibros.erp.datatables.DataTable;
import com.imprimelibros.erp.datatables.DataTablesParser; import com.imprimelibros.erp.datatables.DataTablesParser;
import com.imprimelibros.erp.datatables.DataTablesRequest; import com.imprimelibros.erp.datatables.DataTablesRequest;
import com.imprimelibros.erp.datatables.DataTablesResponse; import com.imprimelibros.erp.datatables.DataTablesResponse;
import com.imprimelibros.erp.payments.model.Payment;
import com.imprimelibros.erp.payments.model.PaymentTransaction;
import com.imprimelibros.erp.payments.model.PaymentTransactionStatus;
import com.imprimelibros.erp.payments.model.PaymentTransactionType;
import com.imprimelibros.erp.users.User;
import com.imprimelibros.erp.users.UserDao; import com.imprimelibros.erp.users.UserDao;
import jakarta.persistence.criteria.Join;
import jakarta.persistence.criteria.JoinType;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
@Controller @Controller
@ -37,11 +33,14 @@ public class PedidosController {
private final PedidoRepository repoPedido; private final PedidoRepository repoPedido;
private final UserDao repoUser; private final UserDao repoUser;
private final MessageSource messageSource; private final MessageSource messageSource;
private final PedidoLineaRepository repoPedidoLinea;
public PedidosController(PedidoRepository repoPedido, UserDao repoUser, MessageSource messageSource) { public PedidosController(PedidoRepository repoPedido, UserDao repoUser, MessageSource messageSource,
PedidoLineaRepository repoPedidoLinea) {
this.repoPedido = repoPedido; this.repoPedido = repoPedido;
this.repoUser = repoUser; this.repoUser = repoUser;
this.messageSource = messageSource; this.messageSource = messageSource;
this.repoPedidoLinea = repoPedidoLinea;
} }
@GetMapping @GetMapping
@ -65,16 +64,15 @@ public class PedidosController {
Long currentUserId = Utils.currentUserId(principal); Long currentUserId = Utils.currentUserId(principal);
List<String> searchable = List.of( List<String> searchable = List.of(
"id", "id"
"estado"
// "client" no, porque lo calculas a posteriori // "client" no, porque lo calculas a posteriori
); );
// Campos ordenables // Campos ordenables
List<String> orderable = List.of( List<String> orderable = List.of(
"id", "id",
"client", "createdBy.fullName",
"created_at", "createdAt",
"total", "total",
"estado"); "estado");
@ -83,6 +81,7 @@ public class PedidosController {
base = base.and((root, query, cb) -> cb.equal(root.get("userId"), currentUserId)); base = base.and((root, query, cb) -> cb.equal(root.get("userId"), currentUserId));
} }
String clientSearch = dt.getColumnSearch("cliente"); String clientSearch = dt.getColumnSearch("cliente");
String estadoSearch = dt.getColumnSearch("estado");
// 2) Si hay filtro, traducirlo a userIds y añadirlo al Specification // 2) Si hay filtro, traducirlo a userIds y añadirlo al Specification
if (clientSearch != null) { if (clientSearch != null) {
@ -92,7 +91,26 @@ public class PedidosController {
// Ningún usuario coincide → forzamos 0 resultados // Ningún usuario coincide → forzamos 0 resultados
base = base.and((root, query, cb) -> cb.disjunction()); base = base.and((root, query, cb) -> cb.disjunction());
} else { } else {
base = base.and((root, query, cb) -> root.get("created_by").in(userIds)); base = base.and((root, query, cb) -> root.get("createdBy").in(userIds));
}
}
if (estadoSearch != null && !estadoSearch.isBlank()) {
try {
PedidoLinea.Estado estadoEnum = PedidoLinea.Estado.valueOf(estadoSearch.trim());
base = base.and((root, query, cb) -> {
// Evitar duplicados de pedidos si el provider usa joins
if (Pedido.class.equals(query.getResultType())) {
query.distinct(true);
}
Join<Pedido, PedidoLinea> lineas = root.join("lineas", JoinType.INNER);
return cb.equal(lineas.get("estado"), estadoEnum);
});
} catch (IllegalArgumentException ex) {
// Valor de estado no válido → forzamos 0 resultados
base = base.and((root, query, cb) -> cb.disjunction());
} }
} }
Long total = repoPedido.count(base); Long total = repoPedido.count(base);
@ -101,11 +119,10 @@ public class PedidosController {
.of(repoPedido, Pedido.class, dt, searchable) .of(repoPedido, Pedido.class, dt, searchable)
.orderable(orderable) .orderable(orderable)
.add("id", Pedido::getId) .add("id", Pedido::getId)
.add("created_at", pedido -> Utils.formatDateTime(pedido.getCreatedAt(), locale)) .add("created_at", pedido -> Utils.formatInstant(pedido.getCreatedAt(), locale))
.add("client", pedido -> { .add("cliente", pedido -> {
if (pedido.getCreatedBy() != null) { if (pedido.getCreatedBy() != null) {
Optional<User> user = repoUser.findById(pedido.getCreatedBy()); return pedido.getCreatedBy().getFullName();
return user.map(User::getFullName).orElse("");
} }
return ""; return "";
}) })
@ -116,9 +133,30 @@ public class PedidosController {
return ""; return "";
} }
}) })
.add("estado", pedido -> {
List<PedidoLinea> lineas = repoPedidoLinea.findByPedidoId(pedido.getId());
if (lineas.isEmpty()) {
return "";
}
// concatenar los estados de las líneas, ordenados por prioridad
StringBuilder sb = new StringBuilder();
lineas.stream()
.map(PedidoLinea::getEstado)
.distinct()
.sorted(Comparator.comparingInt(PedidoLinea.Estado::getPriority))
.forEach(estado -> {
if (sb.length() > 0) {
sb.append(", ");
}
sb.append(messageSource.getMessage(estado.getMessageKey(), null, locale));
});
String text = sb.toString();
return text;
})
.add("actions", pedido -> { .add("actions", pedido -> {
return "<span class=\'badge bg-success btn-view \' data-id=\'" + pedido.getId() + "\' style=\'cursor: pointer;\'>" return "<span class=\'badge bg-success btn-view \' data-id=\'" + pedido.getId()
+ messageSource.getMessage("app.view", null, locale) + "</span>"; + "\' style=\'cursor: pointer;\'>"
+ messageSource.getMessage("app.view", null, locale) + "</span>";
}) })
.where(base) .where(base)
.toJson(total); .toJson(total);

View File

@ -17,4 +17,5 @@ public interface UserService extends UserDetailsService {
* @return página de usuarios * @return página de usuarios
*/ */
Page<User> findByRoleAndSearch(String role, String query, Pageable pageable); Page<User> findByRoleAndSearch(String role, String query, Pageable pageable);
User findById(Long id);
} }

View File

@ -31,4 +31,8 @@ public class UserServiceImpl implements UserService {
if (query == null || query.isBlank()) query = null; if (query == null || query.isBlank()) query = null;
return userDao.searchUsers(role, query, pageable); return userDao.searchUsers(role, query, pageable);
} }
public User findById(Long id) {
return userDao.findById(id).orElse(null);
}
} }

View File

@ -0,0 +1,37 @@
databaseChangeLog:
- changeSet:
id: 0016-fix-enum-estado-pedidos-lineas
author: jjo
changes:
# 1) Convertir valores existentes "maquetación" → "maquetacion"
- update:
tableName: pedidos_lineas
columns:
- column:
name: estado
value: "maquetacion"
where: "estado = 'maquetación'"
# 2) Cambiar ENUM quitando tilde
- modifyDataType:
tableName: pedidos_lineas
columnName: estado
newDataType: "ENUM('aprobado','maquetacion','haciendo_ferro','producción','terminado','cancelado')"
rollback:
# 1) Volver a convertir "maquetacion" → "maquetación"
- update:
tableName: pedidos_lineas
columns:
- column:
name: estado
value: "maquetación"
where: "estado = 'maquetacion'"
# 2) Restaurar ENUM original
- modifyDataType:
tableName: pedidos_lineas
columnName: estado
newDataType: "ENUM('aprobado','maquetación','haciendo_ferro','producción','terminado','cancelado')"

View File

@ -28,4 +28,6 @@ databaseChangeLog:
- include: - include:
file: db/changelog/changesets/0014-create-pedidos-direcciones.yml file: db/changelog/changesets/0014-create-pedidos-direcciones.yml
- include: - include:
file: db/changelog/changesets/0015-alter-pedidos-lineas-and-presupuesto-estados.yml file: db/changelog/changesets/0015-alter-pedidos-lineas-and-presupuesto-estados.yml
- include:
file: db/changelog/changesets/0016-fix-enum-estado-pedidos-lineas.yml

View File

@ -1,3 +1,5 @@
import { normalizeNumericFilter } from '../utils.js';
$(() => { $(() => {
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content'); const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
@ -12,7 +14,7 @@ $(() => {
const language = document.documentElement.lang || 'es-ES'; const language = document.documentElement.lang || 'es-ES';
const tablePedidos = $('#table-pedidos').DataTable({ const tablePedidos = $('#pedidos-datatable').DataTable({
processing: true, processing: true,
serverSide: true, serverSide: true,
orderCellsTop: true, orderCellsTop: true,
@ -41,14 +43,31 @@ $(() => {
url: '/pedidos/datatable', url: '/pedidos/datatable',
method: 'GET', method: 'GET',
}, },
order: [[0, 'desc']], order: [[0, 'desc']],
columns: [ columns: [
{ data: 'id', name: 'id', orderable: true }, { data: 'id', name: 'id', orderable: true },
{ data: 'cliente', name: 'cliente', orderable: true }, { data: 'cliente', name: 'createdBy.fullName', orderable: true },
{ data: 'created_at', name: 'created_at', orderable: true }, { data: 'created_at', name: 'createdAt', orderable: true },
{ data: 'total', name: 'total', orderable: true }, { data: 'total', name: 'total', orderable: true },
{ data: 'estado', name: 'estado', orderable: true },
{ data: 'actions', name: 'actions', orderable: false, searchable: false } { data: 'actions', name: 'actions', orderable: false, searchable: false }
], ],
}); });
tablePedidos.on("keyup change", ".input-filter", function () {
const colName = $(this).data("col");
const colIndex = tablePedidos.settings()[0].aoColumns.findIndex(c => c.name === colName);
if (colIndex >= 0) {
tablePedidos
.column(colIndex)
.search(normalizeNumericFilter(this.value))
.draw();
}
});
$('.btn-view').on('click', function () {
});
}) })

View File

@ -32,20 +32,32 @@
<div class="container-fluid"> <div class="container-fluid">
<table id="pagos-redsys-datatable" class="table table-striped table-nowrap responsive w-100"> <table id="pedidos-datatable" class="table table-striped table-nowrap responsive w-100">
<thead> <thead>
<tr> <tr>
<th scope="col" th:text="#{pedido.table.id}">Num. Pedido</th> <th class="text-start" scope="col" th:text="#{pedido.table.id}">Num. Pedido</th>
<th scope="col" th:text="#{pedido.table.cliente}">Cliente</th> <th class="text-start" scope="col" th:text="#{pedido.table.cliente}">Cliente</th>
<th scope="col" th:text="#{pedido.table.fecha}">Fecha</th> <th class="text-start" scope="col" th:text="#{pedido.table.fecha}">Fecha</th>
<th scope="col" th:text="#{pedido.table.importe}">Importe</th> <th class="text-start" scope="col" th:text="#{pedido.table.importe}">Importe</th>
<th scope="col" th:text="#{pedido.table.acciones}">Acciones</th> <th class="text-start" scope="col" th:text="#{pedido.table.estado}">Estado</th>
<th class="text-start" scope="col" th:text="#{pedido.table.acciones}">Acciones</th>
</tr> </tr>
<tr> <tr>
<th><input type="text" class="form-control form-control-sm input-filter" /></th> <th><input type="text" class="form-control form-control-sm input-filter" data-col="id" /></th>
<th><input type="text" class="form-control form-control-sm input-filter" /></th> <th><input type="text" class="form-control form-control-sm input-filter" data-col="createdBy.fullName" /></th>
<th></th> <th></th>
<th></th> <th></th>
<th>
<select class="form-select form-select-sm input-filter" data-col="estado">
<option value=""></option>
<option th:text="#{pedido.estado.aprobado}" value="aprobado"></option>
<option th:text="#{pedido.estado.maquetacion}" value="maquetacion"></option>
<option th:text="#{pedido.estado.haciendo_ferro}" value="haciendo_ferro"></option>
<option th:text="#{pedido.estado.produccion}" value="produccion"></option>
<option th:text="#{pedido.estado.terminado}" value="terminado"></option>
<option th:text="#{pedido.estado.cancelado}" value="cancelado"></option>
</select>
</th>
<th></th> <!-- Acciones (sin filtro) --> <th></th> <!-- Acciones (sin filtro) -->
</tr> </tr>
</thead> </thead>