package handlers import ( "fmt" "net/http" "os" "strconv" "time" "github.com/go-chi/chi/v5" "github.com/leolionad58/ticketbot/internal/db" ) // TranscriptsHandler serves /transcripts routes. type TranscriptsHandler struct { TicketRepo *db.TicketRepo Renderer *Renderer DiscordInfo DiscordInfo } type transcriptsData struct { baseData Tickets []*db.Ticket Total int Page int Pages int Types []string StaffList []string Guilds []GuildInfo GuildNames map[string]string // Filter values echoed back to the form FStatus string FType string FStaff string FGuild string FFrom string FTo string FSearch string } // HandleList serves GET /transcripts with search/filter/pagination. func (h *TranscriptsHandler) HandleList(w http.ResponseWriter, r *http.Request) { ctx := r.Context() base := h.Renderer.base(r, "transcripts") q := r.URL.Query() fStatus := q.Get("status") fType := q.Get("type") fStaff := q.Get("staff") fGuild := q.Get("guild") fFrom := q.Get("from") fTo := q.Get("to") fSearch := q.Get("search") page, _ := strconv.Atoi(q.Get("page")) if page < 1 { page = 1 } filter := db.TicketFilter{ Status: fStatus, Type: fType, StaffID: fStaff, GuildID: fGuild, Search: fSearch, Page: page, PageSize: 20, } if fFrom != "" { if t, err := time.Parse("2006-01-02", fFrom); err == nil { filter.From = t } } if fTo != "" { if t, err := time.Parse("2006-01-02", fTo); err == nil { filter.To = t.Add(24*time.Hour - time.Second) } } tickets, total, err := h.TicketRepo.ListFiltered(ctx, filter) if err != nil { http.Error(w, "db error: "+err.Error(), http.StatusInternalServerError) return } pages := total / 20 if total%20 != 0 { pages++ } if pages < 1 { pages = 1 } types, _ := h.TicketRepo.ListDistinctTypes(ctx) staff, _ := h.TicketRepo.ListDistinctStaff(ctx) var guilds []GuildInfo guildNames := map[string]string{} if h.DiscordInfo != nil { guilds = h.DiscordInfo.GetGuildList() for _, g := range guilds { guildNames[g.ID] = g.Name } } h.Renderer.Page(w, "transcripts", transcriptsData{ baseData: base, Tickets: tickets, Total: total, Page: page, Pages: pages, Types: types, StaffList: staff, Guilds: guilds, GuildNames: guildNames, FStatus: fStatus, FType: fType, FStaff: fStaff, FGuild: fGuild, FFrom: fFrom, FTo: fTo, FSearch: fSearch, }) } // HandleView serves GET /transcripts/view/{id} — serves the HTML transcript file directly. // This handler must NOT be wrapped with securityHeaders to avoid CSP restrictions. func (h *TranscriptsHandler) HandleView(w http.ResponseWriter, r *http.Request) { ctx := r.Context() id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { http.Error(w, "invalid id", http.StatusBadRequest) return } ticket, err := h.TicketRepo.GetByID(ctx, id) if err != nil || ticket == nil { http.NotFound(w, r) return } if !ticket.TranscriptPath.Valid || ticket.TranscriptPath.String == "" { http.Error(w, "no transcript for this ticket", http.StatusNotFound) return } path := ticket.TranscriptPath.String data, err := os.ReadFile(path) if err != nil { http.Error(w, fmt.Sprintf("transcript file not found: %s", path), http.StatusNotFound) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusOK) _, _ = w.Write(data) } // HandleDownload serves GET /transcripts/download/{id} — serves the HTML transcript as attachment. func (h *TranscriptsHandler) HandleDownload(w http.ResponseWriter, r *http.Request) { ctx := r.Context() id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { http.Error(w, "invalid id", http.StatusBadRequest) return } ticket, err := h.TicketRepo.GetByID(ctx, id) if err != nil || ticket == nil { http.NotFound(w, r) return } if !ticket.TranscriptPath.Valid || ticket.TranscriptPath.String == "" { http.Error(w, "no transcript for this ticket", http.StatusNotFound) return } path := ticket.TranscriptPath.String data, err := os.ReadFile(path) if err != nil { http.Error(w, fmt.Sprintf("transcript file not found: %s", path), http.StatusNotFound) return } filename := fmt.Sprintf("transcript_ticket_%d.html", ticket.TicketNumber) w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename)) w.WriteHeader(http.StatusOK) _, _ = w.Write(data) }