dashboard APIs
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
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))
|
||||
}
|
||||
Reference in New Issue
Block a user