package lkgateway import ( "embed" "fmt" "html/template" "net/http" "path" "strings" "time" "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2mcore" ) //go:embed web/templates/*.html var templatesFS embed.FS // admin содержит по одному *template.Template на каждый view (layout + // конкретный content-шаблон). Так html/template не путается с несколькими // {{define "content"}} в разных файлах. type admin struct { home, claims, claim, status, setup *template.Template } func newAdmin() (*admin, error) { parse := func(content string) (*template.Template, error) { return template.ParseFS(templatesFS, "web/templates/layout.html", "web/templates/"+content) } home, err := parse("admin_home.html") if err != nil { return nil, fmt.Errorf("parse admin_home: %w", err) } claims, err := parse("admin_claims.html") if err != nil { return nil, fmt.Errorf("parse admin_claims: %w", err) } claim, err := parse("admin_claim.html") if err != nil { return nil, fmt.Errorf("parse admin_claim: %w", err) } status, err := parse("admin_status.html") if err != nil { return nil, fmt.Errorf("parse admin_status: %w", err) } setup, err := parse("admin_setup.html") if err != nil { return nil, fmt.Errorf("parse admin_setup: %w", err) } return &admin{home: home, claims: claims, claim: claim, status: status, setup: setup}, nil } // page — общий "конверт" данных для всех шаблонов. type page struct { Title string Active string Now string } // homeData — данные дашборда. type homeData struct { page Status SystemStatus Counts struct { Total int Confirmed int InProgress int Failed int } Recent []ClaimView } // claimsData — данные журнала. type claimsData struct { page Items []ClaimView } // claimData — данные карточки. type claimData struct { page Claim ClaimView } // statusData — данные страницы статуса. type statusData struct { page Checks []Status CheckedAt time.Time } // RegisterAdmin вешает HTML-маршруты /admin/* на mux. Возвращает admin // со всеми загруженными шаблонами — вызывающий может прокинуть его в // registerSetup для добавления вкладки «Настройка». func RegisterAdmin(mux *http.ServeMux, svc *Service, getOpts func() CheckOptions) (*admin, error) { a, err := newAdmin() if err != nil { return nil, err } mux.HandleFunc("/admin/", func(w http.ResponseWriter, r *http.Request) { p := strings.TrimPrefix(r.URL.Path, "/admin/") switch { case p == "" || p == "index" || p == "home": a.renderHome(w, r, svc, getOpts()) case p == "claims": a.renderClaims(w, r, svc) case strings.HasPrefix(p, "claims/"): id := strings.TrimPrefix(p, "claims/") a.renderClaim(w, r, svc, id) case p == "status": a.renderStatus(w, r, getOpts()) default: http.NotFound(w, r) } }) mux.HandleFunc("/admin", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/admin/", http.StatusSeeOther) }) return a, nil } func (a *admin) renderHome(w http.ResponseWriter, r *http.Request, svc *Service, opts CheckOptions) { ctx := r.Context() status := CheckAll(ctx, opts) recent, err := svc.ListClaims(ctx, m2mcore.Filter{Limit: 10}) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } data := homeData{ page: nowPage("Дашборд", "home"), Status: status, Recent: recent.Items, } full, err := svc.ListClaims(ctx, m2mcore.Filter{Limit: 200}) if err == nil { for _, c := range full.Items { data.Counts.Total++ switch c.Status { case string(m2mcore.StateConfirmed), string(m2mcore.StateAwaitingSUB16), string(m2mcore.StateDone): data.Counts.Confirmed++ case string(m2mcore.StateRejected), string(m2mcore.StateTimedOut): data.Counts.Failed++ default: data.Counts.InProgress++ } } } render(w, a.home, data) } func (a *admin) renderClaims(w http.ResponseWriter, r *http.Request, svc *Service) { pageData, err := svc.ListClaims(r.Context(), m2mcore.Filter{Limit: 200}) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } render(w, a.claims, claimsData{page: nowPage("Заявки", "claims"), Items: pageData.Items}) } func (a *admin) renderClaim(w http.ResponseWriter, r *http.Request, svc *Service, id string) { id = path.Base(id) view, err := svc.GetClaim(r.Context(), id) if err != nil { http.Error(w, err.Error(), http.StatusNotFound) return } render(w, a.claim, claimData{page: nowPage("Заявка", "claims"), Claim: view}) } func (a *admin) renderStatus(w http.ResponseWriter, r *http.Request, opts CheckOptions) { s := CheckAll(r.Context(), opts) render(w, a.status, statusData{ page: nowPage("Статус", "status"), Checks: s.Checks, CheckedAt: s.CheckedAt, }) } func render(w http.ResponseWriter, t *template.Template, data any) { w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := t.ExecuteTemplate(w, "layout", data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func nowPage(title, active string) page { return page{Title: title, Active: active, Now: time.Now().Format("02.01.2006 15:04:05")} }