179 lines
5.6 KiB
Go
179 lines
5.6 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
|
|
// @Tags Admin-Dashboard
|
|
// @Produce json
|
|
// @Param closing_window query int false "Hours considered closing soon (default 168)"
|
|
// @Success 200 {object} response.APIResponse{data=SummaryResponse}
|
|
// @Failure 500 {object} response.APIResponse
|
|
// @Router /api/v1/dashboard/summary [get]
|
|
// @Security BearerAuth
|
|
func (h *Handler) Summary(c echo.Context) error {
|
|
query := SummaryQuery{
|
|
ClosingWindow: parseIntQuery(c, "closing_window", 0),
|
|
}
|
|
|
|
out, err := h.service.Summary(c.Request().Context(), query)
|
|
if err != nil {
|
|
return response.InternalServerError(c, "Failed to load dashboard summary")
|
|
}
|
|
|
|
setPrivateCache(c, 60)
|
|
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 /api/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 /api/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 /api/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 /api/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 /api/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")
|
|
}
|
|
|
|
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))
|
|
}
|