diff --git a/rest/controllers/portal_controller.go b/rest/controllers/portal_controller.go
index 10e779e..e411dba 100644
--- a/rest/controllers/portal_controller.go
+++ b/rest/controllers/portal_controller.go
@@ -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())
+}
diff --git a/rest/routes/portal.go b/rest/routes/portal.go
index 794fc76..c07f4c3 100644
--- a/rest/routes/portal.go
+++ b/rest/routes/portal.go
@@ -21,6 +21,7 @@ func PortalRoutes(app fiber.Router) {
portal.Get("/dashboard", controllers.PortalDashboard)
portal.Get("/proyecto/:slug", controllers.PortalProyecto)
+ portal.Get("/proyecto/:slug/cronograma.xlsx", controllers.PortalDescargarCronograma)
// API JSON para Alpine.js
portal.Get("/api/proyectos", controllers.PortalGetProyectos)