feat(portal): descarga del cronograma del proyecto en Excel
El cliente ya veía el roadmap en pantalla pero no tenía cómo pasárselo a alguien que no entra al portal. El .xlsx se genera con archive/zip + encoding/xml de la stdlib en vez de sumar una dependencia de Excel para una sola pantalla. Un CSV renombrado no servía: en Excel en español el separador y los acentos se rompen, y esto es un archivo que el cliente le entrega a un tercero. Detalles que hacen que Excel lo acepte y se lea bien: inline strings (sin tabla sharedStrings, así ningún índice puede apuntar a la cadena equivocada), encabezado en negrita y congelado, anchos de columna según el contenido, y nombre de hoja saneado (Excel rechaza el archivo si pasa de 31 caracteres o trae : \ / ? * [ ]). El endpoint repite el chequeo de acceso de PortalProyecto: sin eso, cualquiera con sesión de portal se bajaba el cronograma de otro cliente adivinando el slug. Verificado abriendo el archivo generado con openpyxl: 6 partes, CRC OK, XML bien formado en todas, acentos y CJK intactos, negrita y panel congelado donde corresponde. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
5182c45d11
commit
62c644b815
@@ -1,6 +1,7 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -8,7 +9,9 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sujit-baniya/fiber-boilerplate/app"
|
||||
@@ -897,3 +900,73 @@ func searchTokenInUpdates(botToken, token string) (int64, bool) {
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// PortalDescargarCronograma entrega el cronograma del proyecto como .xlsx,
|
||||
// para que el cliente pueda mandárselo a alguien que no entra al portal.
|
||||
// Ruta: GET /portal/proyecto/:slug/cronograma.xlsx
|
||||
func PortalDescargarCronograma(c *fiber.Ctx) error {
|
||||
u := middlewares.PortalUserFromLocals(c)
|
||||
if u == nil {
|
||||
return c.Redirect("/portal/login")
|
||||
}
|
||||
proy, err := models.GetProyectoBySlug(c.Params("slug"))
|
||||
if err != nil {
|
||||
return c.Status(404).SendString("proyecto no encontrado")
|
||||
}
|
||||
|
||||
// Mismo chequeo de acceso que PortalProyecto: sin esto, cualquiera con
|
||||
// sesión de portal podría bajarse el cronograma de otro cliente
|
||||
// adivinando el slug.
|
||||
fullUser, _ := models.GetPortalUserByID(u.ID)
|
||||
permitido := false
|
||||
for _, cid := range models.GetClienteIDsForPortalUser(fullUser) {
|
||||
if cid == proy.ClienteID {
|
||||
permitido = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !permitido {
|
||||
return c.Status(403).SendString("sin acceso a este proyecto")
|
||||
}
|
||||
|
||||
fases, _ := models.GetFasesByProyecto(proy.ID)
|
||||
|
||||
estados := map[string]string{
|
||||
"pendiente": "Pendiente",
|
||||
"en_progreso": "En progreso",
|
||||
"completado": "Completado",
|
||||
}
|
||||
fecha := func(t *time.Time) string {
|
||||
if t == nil {
|
||||
return ""
|
||||
}
|
||||
return t.Format("02/01/2006")
|
||||
}
|
||||
|
||||
filas := [][]string{{"#", "Fase", "Descripción", "Estado", "Fecha estimada", "Fecha completado", "Entregables"}}
|
||||
for i, f := range fases {
|
||||
f.ParseEntregables()
|
||||
estado := estados[f.Estado]
|
||||
if estado == "" {
|
||||
estado = f.Estado
|
||||
}
|
||||
filas = append(filas, []string{
|
||||
strconv.Itoa(i + 1),
|
||||
f.Nombre,
|
||||
f.Descripcion,
|
||||
estado,
|
||||
fecha(f.FechaEstimada),
|
||||
fecha(f.FechaCompletado),
|
||||
strings.Join(f.Entregables, ", "),
|
||||
})
|
||||
}
|
||||
|
||||
nombre := fmt.Sprintf("cronograma-%s-%s.xlsx", proy.Slug, time.Now().Format("2006-01-02"))
|
||||
c.Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||
c.Set("Content-Disposition", `attachment; filename="`+nombre+`"`)
|
||||
var buf bytes.Buffer
|
||||
if err := services.EscribirXLSX(&buf, "Cronograma", filas); err != nil {
|
||||
return c.Status(500).SendString("no se pudo generar el archivo")
|
||||
}
|
||||
return c.Send(buf.Bytes())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user