Files
tm_back/internal/dashboard/handler.go
T
Mazyar 541d08a014
continuous-integration/drone/push Build is passing
Enhance dashboard summary and statistics with new fields and caching improvements
- Added new field `Days` and `ScrapedTED` to `SummaryResponse` for tracking daily TED scrape counts.
- Updated `SummaryQuery` to include `Days` parameter for querying scraped TED data.
- Modified `Summary` handler to accept and process the new `Days` parameter.
- Refactored `Summary` method in the repository to support the new `Days` parameter and improved aggregation logic.
- Enhanced caching logic in the service layer to utilize a composite cache key based on `closingWindowSec` and `days`, improving cache management and retrieval efficiency.

This update improves the dashboard's functionality by providing more detailed insights into TED scraping activities and optimizing the caching strategy for better performance.
2026-07-01 23:03:23 +03:30

205 lines
6.7 KiB
Go

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))
}