This commit is contained in:
Lizandro Guarnizo
2026-05-19 13:33:39 -05:00
parent 762ebc2afc
commit b5e6a9285f
4 changed files with 74 additions and 50 deletions
+8 -7
View File
@@ -152,10 +152,10 @@
<div class="bg-white rounded-xl border border-slate-200 p-4 flex items-center gap-4"> <div class="bg-white rounded-xl border border-slate-200 p-4 flex items-center gap-4">
<!-- Icono por tipo --> <!-- Icono por tipo -->
<div class="w-10 h-10 rounded-xl flex items-center justify-center flex-shrink-0" <div class="w-10 h-10 rounded-xl flex items-center justify-center flex-shrink-0"
:class="d.tipo==='contrato' ? 'bg-blue-50' : d.tipo==='orden_servicio' ? 'bg-amber-50' : 'bg-slate-100'"> :class="d.tipo==='contrato' ? 'bg-blue-50 text-blue-500' : d.tipo==='orden_servicio' ? 'bg-amber-50 text-amber-500' : 'bg-slate-100 text-slate-400'">
<svg x-show="d.tipo==='contrato'" class="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg> <svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<svg x-show="d.tipo==='orden_servicio'" class="w-5 h-5 text-amber-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/></svg> <path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
<svg x-show="d.tipo!=='contrato' && d.tipo!=='orden_servicio'" class="w-5 h-5 text-slate-400" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"/></svg> </svg>
</div> </div>
<!-- Info --> <!-- Info -->
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
@@ -163,13 +163,14 @@
<div class="flex items-center gap-2 mt-0.5"> <div class="flex items-center gap-2 mt-0.5">
<span class="text-xs px-2 py-0.5 rounded-full font-medium capitalize" <span class="text-xs px-2 py-0.5 rounded-full font-medium capitalize"
:class="d.tipo==='contrato' ? 'bg-blue-100 text-blue-700' : d.tipo==='orden_servicio' ? 'bg-amber-100 text-amber-700' : 'bg-slate-100 text-slate-500'" :class="d.tipo==='contrato' ? 'bg-blue-100 text-blue-700' : d.tipo==='orden_servicio' ? 'bg-amber-100 text-amber-700' : 'bg-slate-100 text-slate-500'"
x-text="d.tipo?.replace('_',' ')"></span> x-text="(d.tipo||'').replace('_',' ')"></span>
<span x-show="d.descripcion" class="text-xs text-slate-400 truncate" x-text="d.descripcion"></span> <span x-show="d.original_name" class="text-xs text-slate-400 truncate" x-text="d.original_name"></span>
</div> </div>
<p x-show="d.descripcion" class="text-xs text-slate-500 mt-0.5 truncate" x-text="d.descripcion"></p>
</div> </div>
<!-- Descarga --> <!-- Descarga -->
<a :href="`/portal/documentos/${d.ID}/download`" <a :href="`/portal/documentos/${d.ID}/download`"
class="flex-shrink-0 text-[#8eb02f] hover:text-[#6d8c24]" title="Descargar"> class="flex-shrink-0 w-8 h-8 rounded-lg flex items-center justify-center text-[#8eb02f] hover:bg-[#8eb02f]/10 transition-colors" title="Descargar">
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg> <svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
</a> </a>
</div> </div>
+11 -4
View File
@@ -252,7 +252,9 @@ func PortalResponderTicket(c *fiber.Ctx) error {
return c.Status(401).JSON(fiber.Map{"error": "no autenticado"}) return c.Status(401).JSON(fiber.Map{"error": "no autenticado"})
} }
ticketID, _ := c.ParamsInt("id") ticketID, _ := c.ParamsInt("id")
type Req struct{ Contenido string `json:"contenido"` } type Req struct {
Contenido string `json:"contenido"`
}
var req Req var req Req
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()}) return c.Status(400).JSON(fiber.Map{"error": err.Error()})
@@ -382,7 +384,7 @@ func PortalDownloadDocumento(c *fiber.Ctx) error {
if !hasAccess { if !hasAccess {
return c.Status(403).JSON(fiber.Map{"error": "Sin acceso"}) return c.Status(403).JSON(fiber.Map{"error": "Sin acceso"})
} }
clean := doc.Archivo clean := filepath.Clean(doc.Archivo)
if !strings.HasPrefix(clean, "uploads/") { if !strings.HasPrefix(clean, "uploads/") {
return c.Status(403).JSON(fiber.Map{"error": "Acceso denegado"}) return c.Status(403).JSON(fiber.Map{"error": "Acceso denegado"})
} }
@@ -390,7 +392,13 @@ func PortalDownloadDocumento(c *fiber.Ctx) error {
if nombre == "" { if nombre == "" {
nombre = doc.Nombre nombre = doc.Nombre
} }
c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, nombre)) safe := strings.Map(func(r rune) rune {
if r > 127 || r == '"' || r == '\\' || r == '/' || r == '\n' || r == '\r' {
return '_'
}
return r
}, nombre)
c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"; filename*=UTF-8''%s`, safe, url.PathEscape(nombre)))
return c.SendFile(clean) return c.SendFile(clean)
} }
@@ -792,4 +800,3 @@ func searchTokenInUpdates(botToken, token string) (int64, bool) {
} }
return 0, false return 0, false
} }
+17 -1
View File
@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"math" "math"
"mime/multipart" "mime/multipart"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
@@ -491,10 +492,25 @@ func DownloadDocumento(c *fiber.Ctx) error {
if !strings.HasPrefix(clean, "uploads/") { if !strings.HasPrefix(clean, "uploads/") {
return c.Status(403).JSON(fiber.Map{"error": "Acceso denegado"}) return c.Status(403).JSON(fiber.Map{"error": "Acceso denegado"})
} }
c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, item.OriginalName)) nombre := item.OriginalName
if nombre == "" {
nombre = item.Nombre
}
c.Set("Content-Disposition", attachmentDisposition(nombre))
return c.SendFile(clean) return c.SendFile(clean)
} }
// attachmentDisposition genera un header Content-Disposition con fallback ASCII y encoding RFC 5987.
func attachmentDisposition(name string) string {
safe := strings.Map(func(r rune) rune {
if r > 127 || r == '"' || r == '\\' || r == '/' || r == '\n' || r == '\r' {
return '_'
}
return r
}, name)
return fmt.Sprintf(`attachment; filename="%s"; filename*=UTF-8''%s`, safe, url.PathEscape(name))
}
// ─── Tickets (admin) ────────────────────────────────────────────────────────── // ─── Tickets (admin) ──────────────────────────────────────────────────────────
func GetTickets(c *fiber.Ctx) error { func GetTickets(c *fiber.Ctx) error {