527 lines
16 KiB
Go
527 lines
16 KiB
Go
package controllers
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"mime/multipart"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
|
|
)
|
|
|
|
// ─── Admin pages ──────────────────────────────────────────────────────────────
|
|
|
|
func ProyectosIndex(c *fiber.Ctx) error {
|
|
return c.Render("proyectos", fiber.Map{
|
|
"user": c.Locals("user"),
|
|
"modules": c.Locals("userModules"),
|
|
}, "layouts/main")
|
|
}
|
|
|
|
func ProyectoDetalleIndex(c *fiber.Ctx) error {
|
|
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
|
|
if err != nil {
|
|
return c.Redirect("/app/proyectos")
|
|
}
|
|
proy, err := models.GetProyectoByID(uint(id))
|
|
if err != nil {
|
|
return c.Redirect("/app/proyectos")
|
|
}
|
|
return c.Render("proyecto_detalle", fiber.Map{
|
|
"user": c.Locals("user"),
|
|
"modules": c.Locals("userModules"),
|
|
"proyecto": proy,
|
|
}, "layouts/main")
|
|
}
|
|
|
|
// ─── CRUD Proyecto ─────────────────────────────────────────────────────────────
|
|
|
|
func LoadProyectos(c *fiber.Ctx) error {
|
|
page, _ := strconv.Atoi(c.Query("page", "1"))
|
|
search := c.Query("search", "")
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
limit := 20
|
|
offset := (page - 1) * limit
|
|
items, total, err := models.GetAllProyectos(limit, offset, search)
|
|
if err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
// Obtener clientes para el formulario
|
|
clientes, _, _ := models.GetAllClientes(200, 0, "")
|
|
return c.JSON(fiber.Map{
|
|
"items": items,
|
|
"total": total,
|
|
"totalPages": int(math.Ceil(float64(total) / float64(limit))),
|
|
"page": page,
|
|
"clientes": clientes,
|
|
})
|
|
}
|
|
|
|
func CreateProyecto(c *fiber.Ctx) error {
|
|
type Req struct {
|
|
ClienteID string `json:"cliente_id"`
|
|
Nombre string `json:"nombre"`
|
|
Slug string `json:"slug"`
|
|
Descripcion string `json:"descripcion"`
|
|
Color string `json:"color"`
|
|
Estado string `json:"estado"`
|
|
}
|
|
var req Req
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
clienteID, _ := strconv.ParseUint(req.ClienteID, 10, 64)
|
|
if clienteID == 0 || strings.TrimSpace(req.Nombre) == "" {
|
|
return c.Status(400).JSON(fiber.Map{"error": "cliente y nombre son requeridos"})
|
|
}
|
|
if req.Slug == "" {
|
|
req.Slug = slugify(req.Nombre)
|
|
}
|
|
if req.Color == "" {
|
|
req.Color = "#8eb02f"
|
|
}
|
|
if req.Estado == "" {
|
|
req.Estado = "activo"
|
|
}
|
|
p := &models.Proyecto{
|
|
ClienteID: uint(clienteID),
|
|
Nombre: req.Nombre,
|
|
Slug: req.Slug,
|
|
Descripcion: req.Descripcion,
|
|
Color: req.Color,
|
|
Estado: req.Estado,
|
|
}
|
|
if err := models.CreateProyecto(p); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.Status(201).JSON(p)
|
|
}
|
|
|
|
func UpdateProyecto(c *fiber.Ctx) error {
|
|
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
|
|
if err != nil {
|
|
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
|
|
}
|
|
type Req struct {
|
|
ClienteID string `json:"cliente_id"`
|
|
Nombre string `json:"nombre"`
|
|
Slug string `json:"slug"`
|
|
Descripcion string `json:"descripcion"`
|
|
Color string `json:"color"`
|
|
Estado string `json:"estado"`
|
|
Progreso int `json:"progreso"`
|
|
}
|
|
var req Req
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
updateClienteID, _ := strconv.ParseUint(req.ClienteID, 10, 64)
|
|
p := &models.Proyecto{
|
|
ClienteID: uint(updateClienteID),
|
|
Nombre: req.Nombre,
|
|
Slug: req.Slug,
|
|
Descripcion: req.Descripcion,
|
|
Color: req.Color,
|
|
Estado: req.Estado,
|
|
Progreso: req.Progreso,
|
|
}
|
|
p.ID = uint(id)
|
|
if err := models.UpdateProyecto(p); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"ok": true})
|
|
}
|
|
|
|
func DeleteProyecto(c *fiber.Ctx) error {
|
|
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
|
|
if err != nil {
|
|
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
|
|
}
|
|
if err := models.DeleteProyecto(uint(id)); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"ok": true})
|
|
}
|
|
|
|
// ─── Fases ────────────────────────────────────────────────────────────────────
|
|
|
|
func GetFases(c *fiber.Ctx) error {
|
|
id, _ := strconv.ParseUint(c.Params("id"), 10, 32)
|
|
fases, err := models.GetFasesByProyecto(uint(id))
|
|
if err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fases)
|
|
}
|
|
|
|
func CreateFase(c *fiber.Ctx) error {
|
|
proyID, _ := strconv.ParseUint(c.Params("id"), 10, 32)
|
|
type Req struct {
|
|
Nombre string `json:"nombre"`
|
|
Descripcion string `json:"descripcion"`
|
|
Estado string `json:"estado"`
|
|
Orden int `json:"orden"`
|
|
FechaEstimada string `json:"fecha_estimada"`
|
|
Entregables []string `json:"entregables"`
|
|
}
|
|
var req Req
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
f := &models.ProyectoFase{
|
|
ProyectoID: uint(proyID),
|
|
Nombre: req.Nombre,
|
|
Descripcion: req.Descripcion,
|
|
Estado: req.Estado,
|
|
Orden: req.Orden,
|
|
Entregables: req.Entregables,
|
|
}
|
|
if req.FechaEstimada != "" {
|
|
t, err := time.Parse("2006-01-02", req.FechaEstimada)
|
|
if err == nil {
|
|
f.FechaEstimada = &t
|
|
}
|
|
}
|
|
if err := models.CreateProyectoFase(f); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
f.ParseEntregables()
|
|
return c.Status(201).JSON(f)
|
|
}
|
|
|
|
func UpdateFase(c *fiber.Ctx) error {
|
|
faseID, _ := strconv.ParseUint(c.Params("faseID"), 10, 32)
|
|
type Req struct {
|
|
Nombre string `json:"nombre"`
|
|
Descripcion string `json:"descripcion"`
|
|
Estado string `json:"estado"`
|
|
Orden int `json:"orden"`
|
|
FechaEstimada string `json:"fecha_estimada"`
|
|
FechaCompletado string `json:"fecha_completado"`
|
|
Entregables []string `json:"entregables"`
|
|
}
|
|
var req Req
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
f := &models.ProyectoFase{
|
|
Nombre: req.Nombre,
|
|
Descripcion: req.Descripcion,
|
|
Estado: req.Estado,
|
|
Orden: req.Orden,
|
|
Entregables: req.Entregables,
|
|
}
|
|
f.ID = uint(faseID)
|
|
if req.FechaEstimada != "" {
|
|
t, err := time.Parse("2006-01-02", req.FechaEstimada)
|
|
if err == nil {
|
|
f.FechaEstimada = &t
|
|
}
|
|
}
|
|
if req.FechaCompletado != "" {
|
|
t, err := time.Parse("2006-01-02", req.FechaCompletado)
|
|
if err == nil {
|
|
f.FechaCompletado = &t
|
|
}
|
|
}
|
|
// Auto-set fecha_completado when estado = completado
|
|
if req.Estado == "completado" && f.FechaCompletado == nil {
|
|
now := time.Now()
|
|
f.FechaCompletado = &now
|
|
}
|
|
if err := models.UpdateProyectoFase(f); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"ok": true})
|
|
}
|
|
|
|
func DeleteFase(c *fiber.Ctx) error {
|
|
faseID, _ := strconv.ParseUint(c.Params("faseID"), 10, 32)
|
|
if err := models.DeleteProyectoFase(uint(faseID)); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"ok": true})
|
|
}
|
|
|
|
func ApplyTemplate(c *fiber.Ctx) error {
|
|
id, _ := strconv.ParseUint(c.Params("id"), 10, 32)
|
|
if err := models.ApplyFaseTemplate(uint(id)); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
fases, _ := models.GetFasesByProyecto(uint(id))
|
|
return c.JSON(fases)
|
|
}
|
|
|
|
// ─── Avances ──────────────────────────────────────────────────────────────────
|
|
|
|
func GetAvances(c *fiber.Ctx) error {
|
|
id, _ := strconv.ParseUint(c.Params("id"), 10, 32)
|
|
avances, err := models.GetAvancesByProyecto(uint(id), false)
|
|
if err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(avances)
|
|
}
|
|
|
|
func CreateAvance(c *fiber.Ctx) error {
|
|
proyID, _ := strconv.ParseUint(c.Params("id"), 10, 32)
|
|
var a models.ProyectoAvance
|
|
if err := c.BodyParser(&a); err != nil {
|
|
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
a.ProyectoID = uint(proyID)
|
|
if a.Tipo == "" {
|
|
a.Tipo = "update"
|
|
}
|
|
a.Visible = true
|
|
if err := models.CreateProyectoAvance(&a); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.Status(201).JSON(a)
|
|
}
|
|
|
|
func UpdateAvance(c *fiber.Ctx) error {
|
|
avID, _ := strconv.ParseUint(c.Params("avID"), 10, 32)
|
|
var a models.ProyectoAvance
|
|
if err := c.BodyParser(&a); err != nil {
|
|
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
a.ID = uint(avID)
|
|
if err := models.UpdateProyectoAvance(&a); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"ok": true})
|
|
}
|
|
|
|
func DeleteAvance(c *fiber.Ctx) error {
|
|
avID, _ := strconv.ParseUint(c.Params("avID"), 10, 32)
|
|
if err := models.DeleteProyectoAvance(uint(avID)); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"ok": true})
|
|
}
|
|
|
|
// ─── Entregables ──────────────────────────────────────────────────────────────
|
|
|
|
func GetEntregables(c *fiber.Ctx) error {
|
|
id, _ := strconv.ParseUint(c.Params("id"), 10, 32)
|
|
items, err := models.GetEntregablesByProyecto(uint(id), false)
|
|
if err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(items)
|
|
}
|
|
|
|
func UploadEntregable(c *fiber.Ctx) error {
|
|
proyID, _ := strconv.ParseUint(c.Params("id"), 10, 32)
|
|
if _, err := c.MultipartForm(); err != nil {
|
|
return c.Status(400).JSON(fiber.Map{"error": "Formato multipart requerido"})
|
|
}
|
|
nombre := c.FormValue("nombre")
|
|
descripcion := c.FormValue("descripcion")
|
|
version := c.FormValue("version", "1.0")
|
|
|
|
file, err := c.FormFile("archivo")
|
|
if err != nil {
|
|
return c.Status(400).JSON(fiber.Map{"error": "Archivo requerido"})
|
|
}
|
|
if file.Size > 50*1024*1024 {
|
|
return c.Status(400).JSON(fiber.Map{"error": "Archivo máximo 50MB"})
|
|
}
|
|
|
|
dir := fmt.Sprintf("uploads/proyectos/%d", proyID)
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": "Error al crear directorio"})
|
|
}
|
|
|
|
safeFile := safeFilenameProyecto(file.Filename)
|
|
savePath := filepath.Join(dir, safeFile)
|
|
if err := c.SaveFile(file, savePath); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": "Error al guardar archivo"})
|
|
}
|
|
|
|
e := &models.ProyectoEntregable{
|
|
ProyectoID: uint(proyID),
|
|
Nombre: nombre,
|
|
Descripcion: descripcion,
|
|
Archivo: savePath,
|
|
OriginalName: file.Filename,
|
|
Version: version,
|
|
TipoMime: mimeFromHeader(file),
|
|
Tamanio: file.Size,
|
|
Visible: true,
|
|
}
|
|
if err := models.CreateProyectoEntregable(e); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.Status(201).JSON(e)
|
|
}
|
|
|
|
func DeleteEntregable(c *fiber.Ctx) error {
|
|
entID, _ := strconv.ParseUint(c.Params("entID"), 10, 32)
|
|
item, err := models.GetProyectoEntregableByID(uint(entID))
|
|
if err != nil {
|
|
return c.Status(404).JSON(fiber.Map{"error": "No encontrado"})
|
|
}
|
|
_ = os.Remove(item.Archivo)
|
|
if err := models.DeleteProyectoEntregable(uint(entID)); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"ok": true})
|
|
}
|
|
|
|
func DownloadEntregable(c *fiber.Ctx) error {
|
|
entID, _ := strconv.ParseUint(c.Params("entID"), 10, 32)
|
|
item, err := models.GetProyectoEntregableByID(uint(entID))
|
|
if err != nil {
|
|
return c.Status(404).JSON(fiber.Map{"error": "No encontrado"})
|
|
}
|
|
clean := filepath.Clean(item.Archivo)
|
|
if !strings.HasPrefix(clean, "uploads/") {
|
|
return c.Status(403).JSON(fiber.Map{"error": "Acceso denegado"})
|
|
}
|
|
c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, item.OriginalName))
|
|
return c.SendFile(clean)
|
|
}
|
|
|
|
func UpdateEntregableVisibilidad(c *fiber.Ctx) error {
|
|
entID, _ := strconv.ParseUint(c.Params("entID"), 10, 32)
|
|
type Req struct{ Visible bool `json:"visible"` }
|
|
var req Req
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
if err := models.UpdateEntregableVisibilidad(uint(entID), req.Visible); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"ok": true})
|
|
}
|
|
|
|
// ─── Tickets (admin) ──────────────────────────────────────────────────────────
|
|
|
|
func GetTickets(c *fiber.Ctx) error {
|
|
id, _ := strconv.ParseUint(c.Params("id"), 10, 32)
|
|
items, err := models.GetTicketsByProyecto(uint(id))
|
|
if err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(items)
|
|
}
|
|
|
|
func UpdateTicketEstadoAdmin(c *fiber.Ctx) error {
|
|
ticketID, _ := strconv.ParseUint(c.Params("ticketID"), 10, 32)
|
|
type Req struct{ Estado string `json:"estado"` }
|
|
var req Req
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
if err := models.UpdateTicketEstado(uint(ticketID), req.Estado); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"ok": true})
|
|
}
|
|
|
|
func AdminResponderTicket(c *fiber.Ctx) error {
|
|
ticketID, _ := strconv.ParseUint(c.Params("ticketID"), 10, 32)
|
|
type Req struct{ Contenido string `json:"contenido"` }
|
|
var req Req
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
if strings.TrimSpace(req.Contenido) == "" {
|
|
return c.Status(400).JSON(fiber.Map{"error": "El mensaje no puede estar vacío"})
|
|
}
|
|
// Obtener nombre del admin
|
|
adminNombre := "Soporte"
|
|
if u := c.Locals("user"); u != nil {
|
|
if m, ok := u.(map[string]interface{}); ok {
|
|
if n, ok := m["Name"].(string); ok && n != "" {
|
|
adminNombre = n
|
|
}
|
|
}
|
|
}
|
|
msg := &models.TicketMensaje{
|
|
TicketID: uint(ticketID),
|
|
Contenido: req.Contenido,
|
|
EsAdmin: true,
|
|
AutorNombre: adminNombre,
|
|
}
|
|
if err := models.CreateTicketMensaje(msg); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
// Cambiar estado a en_progreso si estaba abierto
|
|
_ = models.UpdateTicketEstado(uint(ticketID), "en_progreso")
|
|
// Notificar al portal user
|
|
ticket, _ := models.GetTicketByID(uint(ticketID))
|
|
if ticket != nil {
|
|
go services.DispatchTicketRespuestaAdmin(ticket, req.Contenido, ticket.Proyecto.Nombre)
|
|
}
|
|
return c.Status(201).JSON(msg)
|
|
}
|
|
|
|
// ─── helpers ──────────────────────────────────────────────────────────────────
|
|
|
|
func slugify(s string) string {
|
|
s = strings.ToLower(s)
|
|
replacer := strings.NewReplacer(
|
|
" ", "-", "á", "a", "é", "e", "í", "i", "ó", "o", "ú", "u",
|
|
"ñ", "n", "ü", "u",
|
|
)
|
|
s = replacer.Replace(s)
|
|
var b strings.Builder
|
|
for _, r := range s {
|
|
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' {
|
|
b.WriteRune(r)
|
|
}
|
|
}
|
|
return strings.Trim(b.String(), "-")
|
|
}
|
|
|
|
func safeFilenameProyecto(name string) string {
|
|
base := filepath.Base(name)
|
|
ext := filepath.Ext(base)
|
|
stem := strings.TrimSuffix(base, ext)
|
|
safe := strings.Map(func(r rune) rune {
|
|
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_' {
|
|
return r
|
|
}
|
|
return '_'
|
|
}, stem)
|
|
ts := strconv.FormatInt(time.Now().UnixNano(), 36)
|
|
return safe + "_" + ts + ext
|
|
}
|
|
|
|
func mimeFromHeader(fh *multipart.FileHeader) string {
|
|
ct := fh.Header.Get("Content-Type")
|
|
if ct == "" {
|
|
return "application/octet-stream"
|
|
}
|
|
return ct
|
|
}
|
|
|
|
// ─── Tickets global (admin) ───────────────────────────────────────────────────
|
|
|
|
func TicketsAdminIndex(c *fiber.Ctx) error {
|
|
return c.Render("tickets_admin", fiber.Map{
|
|
"user": c.Locals("user"),
|
|
"modules": c.Locals("userModules"),
|
|
}, "layouts/main")
|
|
}
|
|
|
|
func GetAllTicketsAdmin(c *fiber.Ctx) error {
|
|
estado := c.Query("estado", "todos")
|
|
items, err := models.GetAllTickets(estado)
|
|
if err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(items)
|
|
}
|