fix(portal): el avance del dashboard mostraba siempre el valor anterior
Auditoría de /portal/dashboard. Cuatro problemas, todos en el mismo bucle: 1. El avance se veía un render atrasado. ActualizarProgresoProyecto corría DESPUÉS de cargar los proyectos: escribía el valor nuevo en la base pero los structs ya cargados seguían con el viejo, que es lo que se renderiza. El usuario veía el cálculo de la visita anterior. 2. N+1 con escrituras en un GET: dos COUNT y un UPDATE por proyecto. Con 10 proyectos, 30 consultas y 10 escrituras por cada carga del dashboard — incluyendo las de cualquier bot que pase. Ahora es UNA consulta agrupada y ninguna escritura; los caminos que tocan una fase ya mantienen la columna al día, así que recalcular en el GET no aportaba nada. 3. Orden aleatorio de los grupos: se recorría un map de Go, así que un partner veía sus clientes en distinto orden en cada recarga. 4. isPartner se decidía con u.Rol mientras el alcance se decidía con Role.EsPortalPartner. Desincronizados, un usuario veía proyectos de varios clientes sin agrupar, o la vista agrupada vacía. Ahora hay una sola definición (PortalUser.EsPartner) con test de los dos sentidos. Además, el error de carga se descartaba con `_` y el usuario terminaba viendo "no tenés proyectos", indistinguible de una caída de la base. Sin hallazgos de seguridad: el chequeo de acceso por cliente está en todas las rutas, la sesión revalida Activo en cada request, y las plantillas no usan x-html ni template.HTML, así que Go escapa todo. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
62c644b815
commit
cfa91a5c09
@@ -327,3 +327,53 @@ func GetProyectoDocumentoByID(id uint) (*ProyectoDocumento, error) {
|
||||
func DeleteProyectoDocumento(id uint) error {
|
||||
return app.Http.Database.DB.Delete(&ProyectoDocumento{}, id).Error
|
||||
}
|
||||
|
||||
// ProgresoPorProyecto calcula el avance de varios proyectos en UNA consulta
|
||||
// agrupada.
|
||||
//
|
||||
// El dashboard hacía esto con ActualizarProgresoProyecto en un bucle: dos
|
||||
// COUNT y un UPDATE por proyecto (3N consultas, N de ellas escrituras en un
|
||||
// GET). Y no servía para lo que se estaba renderizando: los proyectos ya
|
||||
// estaban cargados en memoria, así que el UPDATE iba a la base pero la
|
||||
// pantalla seguía mostrando el valor anterior — el avance se veía siempre un
|
||||
// render atrasado.
|
||||
//
|
||||
// Las escrituras no hacen falta: todos los caminos que tocan una fase ya
|
||||
// llaman a ActualizarProgresoProyecto (ver proyecto_controller y
|
||||
// proyecto_service). Acá solo se lee.
|
||||
func ProgresoPorProyecto(proyectoIDs []uint) map[uint]int {
|
||||
out := make(map[uint]int, len(proyectoIDs))
|
||||
if len(proyectoIDs) == 0 {
|
||||
return out
|
||||
}
|
||||
var filas []struct {
|
||||
ProyectoID uint
|
||||
Total int64
|
||||
Completados int64
|
||||
}
|
||||
app.Http.Database.DB.Model(&ProyectoFase{}).
|
||||
Select("proyecto_id, COUNT(*) AS total, SUM(CASE WHEN estado = 'completado' THEN 1 ELSE 0 END) AS completados").
|
||||
Where("proyecto_id IN ? AND deleted_at IS NULL", proyectoIDs).
|
||||
Group("proyecto_id").Scan(&filas)
|
||||
|
||||
for _, f := range filas {
|
||||
if f.Total > 0 {
|
||||
out[f.ProyectoID] = int((f.Completados * 100) / f.Total)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// AplicarProgreso pone el avance recién calculado en los structs que se van a
|
||||
// renderizar. Un proyecto sin fases queda en 0, que es lo correcto: no hay
|
||||
// nada planificado todavía.
|
||||
func AplicarProgreso(proyectos []Proyecto) {
|
||||
ids := make([]uint, len(proyectos))
|
||||
for i, p := range proyectos {
|
||||
ids[i] = p.ID
|
||||
}
|
||||
progresos := ProgresoPorProyecto(ids)
|
||||
for i := range proyectos {
|
||||
proyectos[i].Progreso = progresos[proyectos[i].ID]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user