541d08a014
continuous-integration/drone/push Build is passing
- 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.
205 lines
6.7 KiB
Go
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))
|
|
}
|