package dashboard import ( "strconv" "tm/pkg/response" "github.com/labstack/echo/v4" ) // Handler handles dashboard HTTP requests. type Handler struct { service Service } // NewHandler creates a dashboard handler. func NewHandler(service Service) *Handler { return &Handler{service: service} } // Summary returns top-level dashboard counters. // @Summary Dashboard summary // @Description Top-level counters for hero pills and stat cards, including daily TED scrape counts // @Tags Admin-Dashboard // @Produce json // @Param closing_window query int false "Hours considered closing soon (default 168)" // @Param days query int false "Days back for scraped TED daily series (default 14, max 90)" // @Success 200 {object} response.APIResponse{data=SummaryResponse} // @Failure 500 {object} response.APIResponse // @Router /admin/v1/dashboard/summary [get] // @Security BearerAuth func (h *Handler) Summary(c echo.Context) error { query := SummaryQuery{ ClosingWindow: parseIntQuery(c, "closing_window", 0), Days: parseIntQuery(c, "days", 0), } out, err := h.service.Summary(c.Request().Context(), query) if err != nil { return response.InternalServerError(c, "Failed to load dashboard summary") } setPrivateCache(c, 300) return response.Success(c, out, "Dashboard summary retrieved successfully") } // Trend returns per-day tender counts for the flow chart. // @Summary Dashboard trend // @Description Per-day counts for the tender flow chart // @Tags Admin-Dashboard // @Produce json // @Param days query int false "Days back (default 14, max 90)" // @Param metric query string false "created|published|awarded (default created)" // @Success 200 {object} response.APIResponse{data=TrendResponse} // @Failure 500 {object} response.APIResponse // @Router /admin/v1/dashboard/trend [get] // @Security BearerAuth func (h *Handler) Trend(c echo.Context) error { query := TrendQuery{ Days: parseIntQuery(c, "days", 0), Metric: c.QueryParam("metric"), } out, err := h.service.Trend(c.Request().Context(), query) if err != nil { return response.InternalServerError(c, "Failed to load dashboard trend") } setPrivateCache(c, 60) return response.Success(c, out, "Dashboard trend retrieved successfully") } // Countries returns country distribution buckets. // @Summary Dashboard countries // @Description Country distribution for donut and ranking list // @Tags Admin-Dashboard // @Produce json // @Param limit query int false "Top N countries (default 6)" // @Success 200 {object} response.APIResponse{data=CountriesResponse} // @Failure 500 {object} response.APIResponse // @Router /admin/v1/dashboard/countries [get] // @Security BearerAuth func (h *Handler) Countries(c echo.Context) error { query := CountriesQuery{ Limit: parseIntQuery(c, "limit", 0), } out, err := h.service.Countries(c.Request().Context(), query) if err != nil { return response.InternalServerError(c, "Failed to load dashboard countries") } setPrivateCache(c, 60) return response.Success(c, out, "Dashboard countries retrieved successfully") } // NoticeTypes returns notice type distribution. // @Summary Dashboard notice types // @Description Notice type mix for stacked bars // @Tags Admin-Dashboard // @Produce json // @Success 200 {object} response.APIResponse{data=NoticeTypesResponse} // @Failure 500 {object} response.APIResponse // @Router /admin/v1/dashboard/notice-types [get] // @Security BearerAuth func (h *Handler) NoticeTypes(c echo.Context) error { out, err := h.service.NoticeTypes(c.Request().Context()) if err != nil { return response.InternalServerError(c, "Failed to load dashboard notice types") } setPrivateCache(c, 60) return response.Success(c, out, "Dashboard notice types retrieved successfully") } // ClosingSoon returns tenders closing within the configured window. // @Summary Dashboard closing soon // @Description Actionable tenders sorted by urgency // @Tags Admin-Dashboard // @Produce json // @Param limit query int false "Max rows (default 5, max 20)" // @Param window query int false "Hours horizon (default 168)" // @Success 200 {object} response.APIResponse{data=ClosingSoonResponse} // @Failure 500 {object} response.APIResponse // @Router /admin/v1/dashboard/closing-soon [get] // @Security BearerAuth func (h *Handler) ClosingSoon(c echo.Context) error { query := ClosingSoonQuery{ Limit: parseIntQuery(c, "limit", 0), Window: parseIntQuery(c, "window", 0), } out, err := h.service.ClosingSoon(c.Request().Context(), query) if err != nil { return response.InternalServerError(c, "Failed to load closing soon tenders") } return response.Success(c, out, "Closing soon tenders retrieved successfully") } // Recent returns the most recently created tenders. // @Summary Dashboard recent tenders // @Description Recent tenders feed sorted by created_at // @Tags Admin-Dashboard // @Produce json // @Param limit query int false "Max rows (default 5, max 20)" // @Param cursor query string false "Opaque pagination cursor" // @Success 200 {object} response.APIResponse{data=RecentResponse} // @Failure 500 {object} response.APIResponse // @Router /admin/v1/dashboard/recent [get] // @Security BearerAuth func (h *Handler) Recent(c echo.Context) error { query := RecentQuery{ Limit: parseIntQuery(c, "limit", 0), Cursor: c.QueryParam("cursor"), } out, err := h.service.Recent(c.Request().Context(), query) if err != nil { return response.InternalServerError(c, "Failed to load recent tenders") } setPrivateCache(c, 60) return response.Success(c, out, "Recent tenders retrieved successfully") } // Statistics returns scraping and translation statistics for charts. // @Summary Dashboard statistics report // @Description Daily and lifetime statistics for document scraping and AI translation // @Tags Admin-Dashboard // @Produce json // @Param days query int false "Days back for daily series (default 14, max 90)" // @Success 200 {object} response.APIResponse{data=StatisticsReportResponse} // @Failure 500 {object} response.APIResponse // @Router /admin/v1/dashboard/statistics [get] // @Security BearerAuth func (h *Handler) Statistics(c echo.Context) error { query := StatisticsQuery{ Days: parseIntQuery(c, "days", 0), } out, err := h.service.Statistics(c.Request().Context(), query) if err != nil { return response.InternalServerError(c, "Failed to load dashboard statistics") } setPrivateCache(c, 300) return response.Success(c, out, "Dashboard statistics retrieved successfully") } func parseIntQuery(c echo.Context, name string, fallback int) int { raw := c.QueryParam(name) if raw == "" { return fallback } v, err := strconv.Atoi(raw) if err != nil { return fallback } return v } func setPrivateCache(c echo.Context, maxAge int) { c.Response().Header().Set("Cache-Control", "private, max-age="+strconv.Itoa(maxAge)) }