package handlers import ( "fmt" "net/http" "strconv" "strings" "github.com/go-chi/chi/v5" "github.com/leolionad58/ticketbot/internal/db" panelauth "github.com/leolionad58/ticketbot/internal/panel/auth" ) // BotSender is the subset of BotService used by PanelsHandler. type BotSender interface { SendPanel(panelID int64) error DeletePanelMessage(panelID int64) error } // GuildRole is a Discord role passed to the panel form. type GuildRole struct { ID string Name string Color string } // GuildChannel is a Discord channel/category passed to the panel form. type GuildChannel struct { ID string Name string Type int // 0=text, 2=voice, 4=category } // GuildInfo is a minimal guild descriptor for the guild selector. type GuildInfo struct { ID string Name string } // DiscordInfo can fetch guild roles and channels for the form dropdowns. type DiscordInfo interface { GetGuildList() []GuildInfo GetGuildRoles(guildID string) ([]GuildRole, error) GetGuildChannels(guildID string) ([]GuildChannel, error) } // PanelsHandler serves all /panels/* routes. type PanelsHandler struct { PanelRepo *db.PanelConfigRepo AuditLog *db.AuditLogRepo BotService BotSender DiscordInfo DiscordInfo Renderer *Renderer } // HandleList serves GET /panels. func (h *PanelsHandler) HandleList(w http.ResponseWriter, r *http.Request) { ctx := r.Context() base := h.Renderer.base(r, "panels") sess := panelauth.SessionFromContext(ctx) if sess != nil { base.CSRFToken = sess.CSRFToken } panels, _ := h.PanelRepo.List(ctx) guildNames := map[string]string{} if h.DiscordInfo != nil { for _, g := range h.DiscordInfo.GetGuildList() { guildNames[g.ID] = g.Name } } h.Renderer.Page(w, "panels_list", struct { baseData Panels []*db.PanelConfig GuildNames map[string]string }{baseData: base, Panels: panels, GuildNames: guildNames}) } // HandleNew serves GET /panels/new. func (h *PanelsHandler) HandleNew(w http.ResponseWriter, r *http.Request) { base := h.Renderer.base(r, "panels") guildID := r.URL.Query().Get("guild_id") if guildID == "" && h.DiscordInfo != nil { if guilds := h.DiscordInfo.GetGuildList(); len(guilds) > 0 { guildID = guilds[0].ID } } h.renderForm(w, base, &db.PanelConfig{EmbedColor: "#5865f2", GuildID: guildID}, true, nil) } // HandleCreate serves POST /panels/new. func (h *PanelsHandler) HandleCreate(w http.ResponseWriter, r *http.Request) { ctx := r.Context() if err := r.ParseForm(); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } base := h.Renderer.base(r, "panels") sess := panelauth.SessionFromContext(ctx) if sess != nil { base.CSRFToken = sess.CSRFToken } p, types, errs := parsePanel(r) if len(errs) > 0 { p.Types = types h.renderForm(w, base, p, true, errs) return } if err := h.PanelRepo.Create(ctx, p); err != nil { if strings.Contains(err.Error(), "UNIQUE") { h.renderForm(w, base, p, true, map[string]string{"name": "Ce nom est déjà utilisé."}) return } http.Error(w, "internal error", http.StatusInternalServerError) return } if len(types) > 0 { if err := h.PanelRepo.ReplaceTypes(ctx, p.ID, types); err != nil { http.Error(w, "internal error", http.StatusInternalServerError) return } } if r.FormValue("action") == "save_send" && h.BotService != nil { _ = h.BotService.SendPanel(p.ID) } http.Redirect(w, r, "/panels", http.StatusSeeOther) } // HandleEdit serves GET /panels/{id}/edit. func (h *PanelsHandler) HandleEdit(w http.ResponseWriter, r *http.Request) { ctx := r.Context() id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) p, err := h.PanelRepo.GetByID(ctx, id) if err != nil || p == nil { http.NotFound(w, r) return } base := h.Renderer.base(r, "panels") sess := panelauth.SessionFromContext(ctx) if sess != nil { base.CSRFToken = sess.CSRFToken } h.renderForm(w, base, p, false, nil) } // HandleUpdate serves POST /panels/{id}/edit. func (h *PanelsHandler) HandleUpdate(w http.ResponseWriter, r *http.Request) { ctx := r.Context() id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err := r.ParseForm(); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } base := h.Renderer.base(r, "panels") sess := panelauth.SessionFromContext(ctx) if sess != nil { base.CSRFToken = sess.CSRFToken } p, types, errs := parsePanel(r) p.ID = id if len(errs) > 0 { p.Types = types h.renderForm(w, base, p, false, errs) return } if err := h.PanelRepo.Update(ctx, p); err != nil { http.Error(w, "internal error", http.StatusInternalServerError) return } if err := h.PanelRepo.ReplaceTypes(ctx, id, types); err != nil { http.Error(w, "internal error", http.StatusInternalServerError) return } if r.FormValue("action") == "save_send" && h.BotService != nil { _ = h.BotService.SendPanel(id) } http.Redirect(w, r, "/panels", http.StatusSeeOther) } // HandleDelete serves POST /panels/{id}/delete. func (h *PanelsHandler) HandleDelete(w http.ResponseWriter, r *http.Request) { ctx := r.Context() id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err := h.PanelRepo.Delete(ctx, id); err != nil { http.Error(w, "internal error", http.StatusInternalServerError) return } http.Redirect(w, r, "/panels", http.StatusSeeOther) } // HandleDuplicate serves POST /panels/{id}/duplicate. func (h *PanelsHandler) HandleDuplicate(w http.ResponseWriter, r *http.Request) { ctx := r.Context() id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) src, err := h.PanelRepo.GetByID(ctx, id) if err != nil || src == nil { http.NotFound(w, r) return } copyP := &db.PanelConfig{ Name: src.Name + "-copie", EmbedTitle: src.EmbedTitle, EmbedDescription: src.EmbedDescription, EmbedColor: src.EmbedColor, GuildID: src.GuildID, } if err := h.PanelRepo.Create(ctx, copyP); err != nil { http.Error(w, "duplicate failed: "+err.Error(), http.StatusInternalServerError) return } // Copy types (without IDs so they get fresh ones) var newTypes []*db.PanelType for _, t := range src.Types { newTypes = append(newTypes, &db.PanelType{ Name: t.Name, ButtonLabel: t.ButtonLabel, ButtonColor: t.ButtonColor, ButtonEmoji: t.ButtonEmoji, EmbedColor: t.EmbedColor, EmbedTitle: t.EmbedTitle, EmbedText: t.EmbedText, StaffRoleID: t.StaffRoleID, CategoryID: t.CategoryID, ClaimChannelID: t.ClaimChannelID, LogChannelID: t.LogChannelID, MaxPerUser: t.MaxPerUser, ClaimMode: t.ClaimMode, CloseRule: t.CloseRule, ModalEnabled: t.ModalEnabled, SortOrder: t.SortOrder, }) } if len(newTypes) > 0 { _ = h.PanelRepo.ReplaceTypes(ctx, copyP.ID, newTypes) } http.Redirect(w, r, fmt.Sprintf("/panels/%d/edit", copyP.ID), http.StatusSeeOther) } // HandlePreview serves POST /panels/preview — returns an HTML fragment. func (h *PanelsHandler) HandlePreview(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } color := r.FormValue("embed_color") if color == "" { color = "#5865f2" } title := r.FormValue("embed_title") if title == "" { title = "Titre du panel" } desc := r.FormValue("embed_description") type btn struct{ Label, Color, Emoji string } var buttons []btn for i := 0; i < 25; i++ { label := r.FormValue(fmt.Sprintf("types[%d][button_label]", i)) if label == "" { break } buttons = append(buttons, btn{ Label: label, Color: r.FormValue(fmt.Sprintf("types[%d][button_color]", i)), Emoji: r.FormValue(fmt.Sprintf("types[%d][button_emoji]", i)), }) } btnColorMap := map[string]string{ "primary": "bg-indigo-600", "secondary": "bg-gray-600", "success": "bg-green-600", "danger": "bg-red-600", } w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprintf(w, `
%s
`, title) if desc != "" { fmt.Fprintf(w, `%s
`, desc) } if len(buttons) > 0 { fmt.Fprint(w, `