318 lines
9.9 KiB
Go
318 lines
9.9 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
|
"github.com/gofiber/fiber/v2"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// GetDashboardReport returns a comprehensive dashboard report
|
|
// @Summary Get dashboard report
|
|
// @Description Returns a comprehensive dashboard report with key metrics
|
|
// @Tags Reports
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param company_id query int false "Company ID filter"
|
|
// @Param branch_id query int false "Branch ID filter"
|
|
// @Param user_id query int false "User ID filter"
|
|
// @Param start_time query string false "Start time filter (RFC3339 format)"
|
|
// @Param end_time query string false "End time filter (RFC3339 format)"
|
|
// @Param sport_id query string false "Sport ID filter"
|
|
// @Param status query int false "Status filter (0=Pending, 1=Win, 2=Loss, 3=Half, 4=Void, 5=Error)"
|
|
// @Security ApiKeyAuth
|
|
// @Success 200 {object} domain.DashboardSummary
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 401 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/reports/dashboard [get]
|
|
func (h *Handler) GetDashboardReport(c *fiber.Ctx) error {
|
|
role := c.Locals("role").(domain.Role)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
// Parse query parameters
|
|
filter, err := parseReportFilter(c, role)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid filter parameters",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
// Get report data
|
|
summary, err := h.reportSvc.GetDashboardSummary(ctx, filter)
|
|
if err != nil {
|
|
h.logger.Error("failed to get dashboard report", "error", err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to generate report",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
res := domain.ConvertDashboardSummaryToRes(summary)
|
|
|
|
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
|
Message: "Dashboard reports generated successfully",
|
|
Success: true,
|
|
StatusCode: 200,
|
|
Data: res,
|
|
})
|
|
|
|
// return c.Status(fiber.StatusOK).JSON(summary)
|
|
}
|
|
|
|
// parseReportFilter parses query parameters into ReportFilter
|
|
func parseReportFilter(c *fiber.Ctx, role domain.Role) (domain.ReportFilter, error) {
|
|
var filter domain.ReportFilter
|
|
var err error
|
|
|
|
if c.Query("company_id") != "" && role == domain.RoleSuperAdmin {
|
|
|
|
companyID, err := strconv.ParseInt(c.Query("company_id"), 10, 64)
|
|
if err != nil {
|
|
return domain.ReportFilter{}, fmt.Errorf("invalid company_id: %w", err)
|
|
}
|
|
filter.CompanyID = domain.ValidInt64{Value: companyID, Valid: true}
|
|
} else {
|
|
filter.CompanyID = c.Locals("company_id").(domain.ValidInt64)
|
|
|
|
}
|
|
|
|
if c.Query("branch_id") != "" && role == domain.RoleSuperAdmin {
|
|
branchID, err := strconv.ParseInt(c.Query("branch_id"), 10, 64)
|
|
if err != nil {
|
|
return domain.ReportFilter{}, fmt.Errorf("invalid branch_id: %w", err)
|
|
}
|
|
filter.BranchID = domain.ValidInt64{Value: branchID, Valid: true}
|
|
} else {
|
|
filter.BranchID = c.Locals("branch_id").(domain.ValidInt64)
|
|
}
|
|
|
|
if c.Query("user_id") != "" {
|
|
userID, err := strconv.ParseInt(c.Query("user_id"), 10, 64)
|
|
if err != nil {
|
|
return domain.ReportFilter{}, fmt.Errorf("invalid user_id: %w", err)
|
|
}
|
|
filter.UserID = domain.ValidInt64{Value: userID, Valid: true}
|
|
}
|
|
|
|
if c.Query("start_time") != "" {
|
|
startTime, err := time.Parse(time.RFC3339, c.Query("start_time"))
|
|
if err != nil {
|
|
return domain.ReportFilter{}, fmt.Errorf("invalid start_time: %w", err)
|
|
}
|
|
filter.StartTime = domain.ValidTime{Value: startTime, Valid: true}
|
|
}
|
|
|
|
if c.Query("end_time") != "" {
|
|
endTime, err := time.Parse(time.RFC3339, c.Query("end_time"))
|
|
if err != nil {
|
|
return domain.ReportFilter{}, fmt.Errorf("invalid end_time: %w", err)
|
|
}
|
|
filter.EndTime = domain.ValidTime{Value: endTime, Valid: true}
|
|
}
|
|
|
|
if c.Query("sport_id") != "" {
|
|
filter.SportID = domain.ValidString{Value: c.Query("sport_id"), Valid: true}
|
|
}
|
|
|
|
if c.Query("status") != "" {
|
|
status, err := strconv.ParseInt(c.Query("status"), 10, 32)
|
|
if err != nil {
|
|
return domain.ReportFilter{}, fmt.Errorf("invalid status: %w", err)
|
|
}
|
|
filter.Status = domain.ValidOutcomeStatus{Value: domain.OutcomeStatus(status), Valid: true}
|
|
}
|
|
|
|
return filter, err
|
|
}
|
|
|
|
// DownloadReportFile godoc
|
|
// @Summary Download a CSV report file
|
|
// @Description Downloads a generated report CSV file from the server
|
|
// @Tags Reports
|
|
// @Param filename path string true "Name of the report file to download (e.g., report_daily_2025-06-21.csv)"
|
|
// @Produce text/csv
|
|
// @Success 200 {file} file "CSV file will be downloaded"
|
|
// @Failure 400 {object} domain.ErrorResponse "Missing or invalid filename"
|
|
// @Failure 404 {object} domain.ErrorResponse "Report file not found"
|
|
// @Failure 500 {object} domain.ErrorResponse "Internal server error while serving the file"
|
|
// @Router /api/v1/report-files/download/{filename} [get]
|
|
func (h *Handler) DownloadReportFile(c *fiber.Ctx) error {
|
|
filename := c.Params("filename")
|
|
if filename == "" || strings.Contains(filename, "..") {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid filename parameter",
|
|
Error: "filename is required and must not contain '..'",
|
|
})
|
|
}
|
|
|
|
reportDir := "reports"
|
|
|
|
// Ensure reports directory exists
|
|
if _, err := os.Stat(reportDir); os.IsNotExist(err) {
|
|
if err := os.MkdirAll(reportDir, os.ModePerm); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to create report directory",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
}
|
|
|
|
filePath := fmt.Sprintf("%s/%s", reportDir, filename)
|
|
|
|
// Check if the report file exists
|
|
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
Message: "Report file not found",
|
|
Error: "no such file",
|
|
})
|
|
}
|
|
|
|
// Set download headers
|
|
c.Set("Content-Type", "text/csv")
|
|
c.Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
|
|
|
|
// Serve the file
|
|
if err := c.SendFile(filePath); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to serve file",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ListReportFiles godoc
|
|
// @Summary List available report CSV files
|
|
// @Description Returns a paginated list of generated report CSV files with search capability
|
|
// @Tags Reports
|
|
// @Produce json
|
|
// @Param search query string false "Search term to filter filenames"
|
|
// @Param page query int false "Page number (default: 1)" default(1)
|
|
// @Param limit query int false "Items per page (default: 20, max: 100)" default(20)
|
|
// @Success 200 {object} domain.PaginatedFileResponse "Paginated list of CSV report filenames"
|
|
// @Failure 400 {object} domain.ErrorResponse "Invalid pagination parameters"
|
|
// @Failure 500 {object} domain.ErrorResponse "Failed to read report directory"
|
|
// @Router /api/v1/report-files/list [get]
|
|
func (h *Handler) ListReportFiles(c *fiber.Ctx) error {
|
|
reportDir := "reports"
|
|
searchTerm := c.Query("search")
|
|
page := c.QueryInt("page", 1)
|
|
limit := c.QueryInt("limit", 20)
|
|
|
|
// Validate pagination parameters
|
|
if page < 1 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid page number",
|
|
Error: "Page must be greater than 0",
|
|
})
|
|
}
|
|
|
|
if limit < 1 || limit > 100 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid limit value",
|
|
Error: "Limit must be between 1 and 100",
|
|
})
|
|
}
|
|
|
|
// Create the reports directory if it doesn't exist
|
|
if _, err := os.Stat(reportDir); os.IsNotExist(err) {
|
|
if err := os.MkdirAll(reportDir, os.ModePerm); err != nil {
|
|
h.mongoLoggerSvc.Error("failed to create report directory",
|
|
zap.Int64("status_code", fiber.StatusInternalServerError),
|
|
zap.Error(err),
|
|
zap.Time("timestamp", time.Now()),
|
|
)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to create report directory",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
}
|
|
|
|
files, err := os.ReadDir(reportDir)
|
|
if err != nil {
|
|
h.mongoLoggerSvc.Error("failed to read report directory",
|
|
zap.Int64("status_code", fiber.StatusInternalServerError),
|
|
zap.Error(err),
|
|
zap.Time("timestamp", time.Now()),
|
|
)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to read report directory",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
var allFiles []string
|
|
for _, file := range files {
|
|
if !file.IsDir() && strings.HasSuffix(file.Name(), ".csv") {
|
|
// Apply search filter if provided
|
|
if searchTerm == "" || strings.Contains(strings.ToLower(file.Name()), strings.ToLower(searchTerm)) {
|
|
allFiles = append(allFiles, file.Name())
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort files by name (descending to show newest first)
|
|
sort.Slice(allFiles, func(i, j int) bool {
|
|
return allFiles[i] > allFiles[j]
|
|
})
|
|
|
|
// Calculate pagination values
|
|
total := len(allFiles)
|
|
startIdx := (page - 1) * limit
|
|
endIdx := startIdx + limit
|
|
|
|
// Adjust end index if it exceeds the slice length
|
|
if endIdx > total {
|
|
endIdx = total
|
|
}
|
|
|
|
// Handle case where start index is beyond available items
|
|
if startIdx >= total {
|
|
return c.Status(fiber.StatusOK).JSON(domain.PaginatedFileResponse{
|
|
Response: domain.Response{
|
|
StatusCode: fiber.StatusOK,
|
|
Message: "No files found for the requested page",
|
|
Success: true,
|
|
},
|
|
Data: []string{},
|
|
Pagination: domain.Pagination{
|
|
Total: total,
|
|
TotalPages: int(math.Ceil(float64(total) / float64(limit))),
|
|
CurrentPage: page,
|
|
Limit: limit,
|
|
},
|
|
})
|
|
}
|
|
|
|
paginatedFiles := allFiles[startIdx:endIdx]
|
|
|
|
return c.Status(fiber.StatusOK).JSON(domain.PaginatedFileResponse{
|
|
Response: domain.Response{
|
|
StatusCode: fiber.StatusOK,
|
|
Message: "Report files retrieved successfully",
|
|
Success: true,
|
|
},
|
|
Data: paginatedFiles,
|
|
Pagination: domain.Pagination{
|
|
Total: total,
|
|
TotalPages: int(math.Ceil(float64(total) / float64(limit))),
|
|
CurrentPage: page,
|
|
Limit: limit,
|
|
},
|
|
})
|
|
}
|