package handlers import ( "context" "fmt" "math" "os" "sort" "strconv" "strings" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "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 := "C:/Users/User/Desktop/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 := "C:/Users/User/Desktop/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, }, }) } func (h *Handler) CreateReportRequest(c *fiber.Ctx) error { userID := c.Locals("user_id").(int64) companyID := c.Locals("company_id").(domain.ValidInt64) var req domain.CreateReportRequestReq if err := c.BodyParser(&req); err != nil { h.BadRequestLogger().Error( "Failed to parse CreateReportRequestReq", zap.Error(err), ) return fiber.NewError(fiber.StatusBadRequest, "Invalid request:", err.Error()) } valErrs, ok := h.validator.Validate(c, req) if !ok { var errMsg string for field, msg := range valErrs { errMsg += fmt.Sprintf("%s: %s; ", field, msg) } h.BadRequestLogger().Error( "Failed to validate CreateReportRequestReq", zap.String("errMsg", errMsg), ) return fiber.NewError(fiber.StatusBadRequest, errMsg) } request, err := h.reportSvc.CreateReportRequest(c.Context(), domain.CreateReportRequest{ CompanyID: companyID, RequestedBy: domain.ValidInt64{ Value: userID, Valid: true, }, Type: domain.ReportRequestType(req.Type), Metadata: req.Metadata, }) if err != nil { h.InternalServerErrorLogger().Error("Failed to create report request", zap.Error(err)) return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } res := domain.ConvertReportRequest(request) return response.WriteJSON(c, fiber.StatusOK, "Report Request has been created", res, nil) } func (h *Handler) GetAllReportRequests(c *fiber.Ctx) error { companyID := c.Locals("company_id").(domain.ValidInt64) page := c.QueryInt("page", 1) pageSize := c.QueryInt("page_size", 10) limit := domain.ValidInt32{ Value: int32(pageSize), Valid: true, } offset := domain.ValidInt32{ Value: int32(page - 1), Valid: true, } statusQuery := c.Query("status") var reportStatus domain.ValidReportRequestStatus if statusQuery != "" { reportStatusParsed, err := domain.ParseReportRequestStatus(statusQuery) if err != nil { h.BadRequestLogger().Error("Failed to parse statusQuery", zap.String("status", statusQuery), zap.Error(err), ) return fiber.NewError(fiber.StatusBadRequest, "invalid report status") } reportStatus = domain.ValidReportRequestStatus{ Value: reportStatusParsed, Valid: true, } } typeQuery := c.Query("type") var reportType domain.ValidReportRequestType if typeQuery != "" { reportTypeParsed, err := domain.ParseReportRequestType(typeQuery) if err != nil { h.BadRequestLogger().Error("Failed to parse typeQuery", zap.String("type", typeQuery), zap.Error(err), ) return fiber.NewError(fiber.StatusBadRequest, "invalid report type") } reportType = domain.ValidReportRequestType{ Value: reportTypeParsed, Valid: true, } } requesterQuery := c.Query("requester") var requestedBy domain.ValidInt64 if requesterQuery != "" { parsedRequestedBy, err := strconv.ParseInt(requesterQuery, 10, 64) if err != nil { h.BadRequestLogger().Error("Failed to parse requester", zap.String("requester", requesterQuery), zap.Error(err), ) return fiber.NewError(fiber.StatusBadRequest, "invalid report requester") } requestedBy = domain.ValidInt64{ Value: parsedRequestedBy, Valid: true, } } requests, total, err := h.reportSvc.GetAllReportRequests(c.Context(), domain.ReportRequestFilter{ CompanyID: companyID, Limit: limit, Offset: offset, Status: reportStatus, Type: reportType, RequestedBy: requestedBy, }) if err != nil { h.InternalServerErrorLogger().Error("Failed to retrieve all report requests", zap.Error(err), ) return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } res := domain.ConvertReportRequestDetailList(requests) return response.WritePaginatedJSON(c, fiber.StatusOK, "All Report Requests successfully retrieved", res, nil, page, int(total)) } func (h *Handler) DownloadReportByID(c *fiber.Ctx) error { requestID := c.Params("id") id, err := strconv.ParseInt(requestID, 10, 64) if err != nil { h.BadRequestLogger().Info("Invalid report request ID", zap.String("requestID", requestID), zap.Error(err), ) return fiber.NewError(fiber.StatusBadRequest, "Invalid request ID") } file, err := h.reportSvc.CheckAndFetchReportFile(c.Context(), id) if err != nil { h.InternalServerErrorLogger().Error("Failed to check and fetch report file", zap.Error(err), zap.String("requestID", requestID), ) return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to check and fetch report file:%v", err.Error())) } c.Set("Content-Type", "text/csv") c.Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", file)) if err := c.SendFile(file); err != nil { h.InternalServerErrorLogger().Error("Unable to download report file", zap.Error(err), zap.String("requestID", requestID), ) return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Unable to download report file:%v", err.Error())) } return nil }