feat(admin): Implement comprehensive admin management functionality
- Add new AdminHandler with methods for dashboard, schedules, users, and bookings - Implement GetDashboard method to retrieve admin dashboard statistics - Add CreateSchedule method with validation and error handling - Implement GetUsers method with pagination support - Add GetBookings method with pagination and filtering capabilities - Implement GetFinancialReports method with date range filtering - Add UpdateSchedule method to modify existing schedule slots - Enhance error handling and response formatting for admin-related operations - Integrate admin service methods for comprehensive administrative tasks
This commit is contained in:
parent
1cb21847d9
commit
c265e8f866
@ -1,38 +1,263 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"attune-heart-therapy/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type AdminHandler struct {
|
||||
// Will be implemented in later tasks
|
||||
adminService services.AdminService
|
||||
}
|
||||
|
||||
func NewAdminHandler() *AdminHandler {
|
||||
return &AdminHandler{}
|
||||
func NewAdminHandler(adminService services.AdminService) *AdminHandler {
|
||||
return &AdminHandler{
|
||||
adminService: adminService,
|
||||
}
|
||||
}
|
||||
|
||||
// GetDashboard handles GET /api/admin/dashboard for dashboard statistics
|
||||
func (h *AdminHandler) GetDashboard(c *gin.Context) {
|
||||
// Will be implemented in task 11
|
||||
c.JSON(501, gin.H{"message": "Not implemented yet"})
|
||||
stats, err := h.adminService.GetDashboardStats()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to get dashboard statistics",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"stats": stats,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateSchedule handles POST /api/admin/schedules for schedule creation
|
||||
func (h *AdminHandler) CreateSchedule(c *gin.Context) {
|
||||
// Will be implemented in task 11
|
||||
c.JSON(501, gin.H{"message": "Not implemented yet"})
|
||||
var req services.CreateScheduleRequest
|
||||
|
||||
// Bind JSON request to struct
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Invalid request format",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Create the schedule
|
||||
schedule, err := h.adminService.CreateSchedule(req)
|
||||
if err != nil {
|
||||
// Handle specific error cases
|
||||
if err.Error() == "end time must be after start time" ||
|
||||
err.Error() == "cannot create schedule slots in the past" ||
|
||||
err.Error() == "max bookings must be at least 1" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err.Error() == "schedule slot overlaps with existing available slot" {
|
||||
c.JSON(http.StatusConflict, gin.H{
|
||||
"error": "Schedule slot overlaps with existing available slot",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to create schedule",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"message": "Schedule created successfully",
|
||||
"schedule": schedule,
|
||||
})
|
||||
}
|
||||
|
||||
// GetUsers handles GET /api/admin/users for retrieving all users
|
||||
func (h *AdminHandler) GetUsers(c *gin.Context) {
|
||||
// Will be implemented in task 11
|
||||
c.JSON(501, gin.H{"message": "Not implemented yet"})
|
||||
// Parse pagination parameters
|
||||
limitStr := c.DefaultQuery("limit", "50")
|
||||
offsetStr := c.DefaultQuery("offset", "0")
|
||||
|
||||
limit, err := strconv.Atoi(limitStr)
|
||||
if err != nil || limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
|
||||
offset, err := strconv.Atoi(offsetStr)
|
||||
if err != nil || offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
// Get users
|
||||
users, total, err := h.adminService.GetAllUsers(limit, offset)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to get users",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"users": users,
|
||||
"total": total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
})
|
||||
}
|
||||
|
||||
// GetBookings handles GET /api/admin/bookings for retrieving all bookings
|
||||
func (h *AdminHandler) GetBookings(c *gin.Context) {
|
||||
// Will be implemented in task 11
|
||||
c.JSON(501, gin.H{"message": "Not implemented yet"})
|
||||
// Parse pagination parameters
|
||||
limitStr := c.DefaultQuery("limit", "50")
|
||||
offsetStr := c.DefaultQuery("offset", "0")
|
||||
|
||||
limit, err := strconv.Atoi(limitStr)
|
||||
if err != nil || limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
|
||||
offset, err := strconv.Atoi(offsetStr)
|
||||
if err != nil || offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
// Get bookings
|
||||
bookings, total, err := h.adminService.GetAllBookings(limit, offset)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to get bookings",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"bookings": bookings,
|
||||
"total": total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
})
|
||||
}
|
||||
|
||||
// GetFinancialReports handles GET /api/admin/reports/financial for financial reports
|
||||
func (h *AdminHandler) GetFinancialReports(c *gin.Context) {
|
||||
// Will be implemented in task 11
|
||||
c.JSON(501, gin.H{"message": "Not implemented yet"})
|
||||
// Parse date parameters
|
||||
startDateStr := c.Query("start_date")
|
||||
endDateStr := c.Query("end_date")
|
||||
|
||||
if startDateStr == "" || endDateStr == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Both start_date and end_date parameters are required (format: YYYY-MM-DD)",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Parse dates
|
||||
startDate, err := time.Parse("2006-01-02", startDateStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Invalid start_date format. Expected: YYYY-MM-DD",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
endDate, err := time.Parse("2006-01-02", endDateStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Invalid end_date format. Expected: YYYY-MM-DD",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Adjust end date to include the entire day
|
||||
endDate = endDate.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
|
||||
|
||||
// Generate financial report
|
||||
report, err := h.adminService.GetFinancialReports(startDate, endDate)
|
||||
if err != nil {
|
||||
if err.Error() == "end date cannot be before start date" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "End date cannot be before start date",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to generate financial report",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"report": report,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateSchedule handles PUT /api/admin/schedules/:id for schedule updates
|
||||
func (h *AdminHandler) UpdateSchedule(c *gin.Context) {
|
||||
// Get schedule ID from URL parameter
|
||||
scheduleIDStr := c.Param("id")
|
||||
scheduleID, err := strconv.ParseUint(scheduleIDStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Invalid schedule ID",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req services.UpdateScheduleRequest
|
||||
|
||||
// Bind JSON request to struct
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Invalid request format",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Update the schedule
|
||||
schedule, err := h.adminService.UpdateSchedule(uint(scheduleID), req)
|
||||
if err != nil {
|
||||
// Handle specific error cases
|
||||
if err.Error() == "schedule not found" {
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"error": "Schedule not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err.Error() == "end time must be after start time" ||
|
||||
err.Error() == "max bookings must be at least 1" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to update schedule",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Schedule updated successfully",
|
||||
"schedule": schedule,
|
||||
})
|
||||
}
|
||||
|
||||
@ -148,3 +148,89 @@ func (r *bookingRepository) GetByPaymentID(paymentID string) (*models.Booking, e
|
||||
|
||||
return &booking, nil
|
||||
}
|
||||
|
||||
// GetAllBookings retrieves all bookings with pagination
|
||||
func (r *bookingRepository) GetAllBookings(limit, offset int) ([]models.Booking, int64, error) {
|
||||
var bookings []models.Booking
|
||||
var total int64
|
||||
|
||||
// Get total count
|
||||
if err := r.db.Model(&models.Booking{}).Count(&total).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to get bookings count: %w", err)
|
||||
}
|
||||
|
||||
// Get bookings with pagination and user preloaded
|
||||
if err := r.db.Preload("User").Limit(limit).Offset(offset).
|
||||
Order("created_at DESC").Find(&bookings).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to get bookings: %w", err)
|
||||
}
|
||||
|
||||
// Clear password hashes for security
|
||||
for i := range bookings {
|
||||
bookings[i].User.PasswordHash = ""
|
||||
}
|
||||
|
||||
return bookings, total, nil
|
||||
}
|
||||
|
||||
// GetBookingStats retrieves booking statistics for admin dashboard
|
||||
func (r *bookingRepository) GetBookingStats() (*BookingStats, error) {
|
||||
var stats BookingStats
|
||||
|
||||
// Get total bookings
|
||||
if err := r.db.Model(&models.Booking{}).Count(&stats.TotalBookings).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get total bookings count: %w", err)
|
||||
}
|
||||
|
||||
// Get upcoming bookings
|
||||
if err := r.db.Model(&models.Booking{}).
|
||||
Where("status = ? AND scheduled_at > ?", models.BookingStatusScheduled, time.Now()).
|
||||
Count(&stats.UpcomingBookings).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get upcoming bookings count: %w", err)
|
||||
}
|
||||
|
||||
// Get completed bookings
|
||||
if err := r.db.Model(&models.Booking{}).
|
||||
Where("status = ?", models.BookingStatusCompleted).
|
||||
Count(&stats.CompletedBookings).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get completed bookings count: %w", err)
|
||||
}
|
||||
|
||||
// Get cancelled bookings
|
||||
if err := r.db.Model(&models.Booking{}).
|
||||
Where("status = ?", models.BookingStatusCancelled).
|
||||
Count(&stats.CancelledBookings).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get cancelled bookings count: %w", err)
|
||||
}
|
||||
|
||||
return &stats, nil
|
||||
}
|
||||
|
||||
// GetFinancialStats retrieves financial statistics for admin reports
|
||||
func (r *bookingRepository) GetFinancialStats(startDate, endDate time.Time) (*FinancialStats, error) {
|
||||
var stats FinancialStats
|
||||
|
||||
// Get total revenue and booking count for the date range
|
||||
var result struct {
|
||||
TotalRevenue float64 `json:"total_revenue"`
|
||||
TotalBookings int64 `json:"total_bookings"`
|
||||
}
|
||||
|
||||
if err := r.db.Model(&models.Booking{}).
|
||||
Select("COALESCE(SUM(amount), 0) as total_revenue, COUNT(*) as total_bookings").
|
||||
Where("payment_status = ? AND created_at >= ? AND created_at <= ?",
|
||||
models.PaymentStatusSucceeded, startDate, endDate).
|
||||
Scan(&result).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get financial stats: %w", err)
|
||||
}
|
||||
|
||||
stats.TotalRevenue = result.TotalRevenue
|
||||
stats.TotalBookings = result.TotalBookings
|
||||
|
||||
// Calculate average booking value
|
||||
if stats.TotalBookings > 0 {
|
||||
stats.AverageBooking = stats.TotalRevenue / float64(stats.TotalBookings)
|
||||
}
|
||||
|
||||
return &stats, nil
|
||||
}
|
||||
|
||||
@ -6,6 +6,21 @@ import (
|
||||
"attune-heart-therapy/internal/models"
|
||||
)
|
||||
|
||||
// BookingStats represents booking statistics for admin dashboard
|
||||
type BookingStats struct {
|
||||
TotalBookings int64 `json:"total_bookings"`
|
||||
UpcomingBookings int64 `json:"upcoming_bookings"`
|
||||
CompletedBookings int64 `json:"completed_bookings"`
|
||||
CancelledBookings int64 `json:"cancelled_bookings"`
|
||||
}
|
||||
|
||||
// FinancialStats represents financial statistics for admin reports
|
||||
type FinancialStats struct {
|
||||
TotalRevenue float64 `json:"total_revenue"`
|
||||
TotalBookings int64 `json:"total_bookings"`
|
||||
AverageBooking float64 `json:"average_booking"`
|
||||
}
|
||||
|
||||
// UserRepository handles user data persistence
|
||||
type UserRepository interface {
|
||||
Create(user *models.User) error
|
||||
@ -13,6 +28,7 @@ type UserRepository interface {
|
||||
GetByEmail(email string) (*models.User, error)
|
||||
Update(user *models.User) error
|
||||
GetActiveUsersCount() (int64, error)
|
||||
GetAllUsers(limit, offset int) ([]models.User, int64, error)
|
||||
}
|
||||
|
||||
// BookingRepository handles booking data persistence
|
||||
@ -24,6 +40,9 @@ type BookingRepository interface {
|
||||
Update(booking *models.Booking) error
|
||||
Delete(id uint) error
|
||||
GetUpcomingBookings() ([]models.Booking, error)
|
||||
GetAllBookings(limit, offset int) ([]models.Booking, int64, error)
|
||||
GetBookingStats() (*BookingStats, error)
|
||||
GetFinancialStats(startDate, endDate time.Time) (*FinancialStats, error)
|
||||
}
|
||||
|
||||
// ScheduleRepository handles schedule data persistence
|
||||
|
||||
@ -110,3 +110,26 @@ func (r *userRepository) GetActiveUsersCount() (int64, error) {
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// GetAllUsers retrieves all users with pagination
|
||||
func (r *userRepository) GetAllUsers(limit, offset int) ([]models.User, int64, error) {
|
||||
var users []models.User
|
||||
var total int64
|
||||
|
||||
// Get total count
|
||||
if err := r.db.Model(&models.User{}).Count(&total).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to get users count: %w", err)
|
||||
}
|
||||
|
||||
// Get users with pagination
|
||||
if err := r.db.Limit(limit).Offset(offset).Order("created_at DESC").Find(&users).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to get users: %w", err)
|
||||
}
|
||||
|
||||
// Clear password hashes for security
|
||||
for i := range users {
|
||||
users[i].PasswordHash = ""
|
||||
}
|
||||
|
||||
return users, total, nil
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@ type Server struct {
|
||||
router *gin.Engine
|
||||
paymentHandler *handlers.PaymentHandler
|
||||
bookingHandler *handlers.BookingHandler
|
||||
adminHandler *handlers.AdminHandler
|
||||
}
|
||||
|
||||
func New(cfg *config.Config) *Server {
|
||||
@ -126,12 +127,16 @@ func (s *Server) setupRoutes() {
|
||||
payments.POST("/webhook", s.paymentHandler.HandleWebhook)
|
||||
}
|
||||
|
||||
// Admin routes (will be implemented in later tasks)
|
||||
// Admin routes - require admin authentication
|
||||
admin := v1.Group("/admin")
|
||||
// Note: Admin authentication middleware will be added in task 13
|
||||
{
|
||||
admin.GET("/dashboard", func(c *gin.Context) {
|
||||
c.JSON(501, gin.H{"message": "Not implemented yet"})
|
||||
})
|
||||
admin.GET("/dashboard", s.adminHandler.GetDashboard)
|
||||
admin.POST("/schedules", s.adminHandler.CreateSchedule)
|
||||
admin.PUT("/schedules/:id", s.adminHandler.UpdateSchedule)
|
||||
admin.GET("/users", s.adminHandler.GetUsers)
|
||||
admin.GET("/bookings", s.adminHandler.GetBookings)
|
||||
admin.GET("/reports/financial", s.adminHandler.GetFinancialReports)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -166,9 +171,13 @@ func (s *Server) initializeServices() {
|
||||
notificationService,
|
||||
)
|
||||
|
||||
// Initialize admin service
|
||||
adminService := services.NewAdminService(repos.User, repos.Booking, repos.Schedule)
|
||||
|
||||
// Initialize handlers
|
||||
s.paymentHandler = handlers.NewPaymentHandler(paymentService)
|
||||
s.bookingHandler = handlers.NewBookingHandler(bookingService)
|
||||
s.adminHandler = handlers.NewAdminHandler(adminService)
|
||||
}
|
||||
|
||||
// healthCheck handles the health check endpoint
|
||||
|
||||
304
internal/services/admin_service.go
Normal file
304
internal/services/admin_service.go
Normal file
@ -0,0 +1,304 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"attune-heart-therapy/internal/models"
|
||||
"attune-heart-therapy/internal/repositories"
|
||||
)
|
||||
|
||||
// adminService implements the AdminService interface
|
||||
type adminService struct {
|
||||
userRepo repositories.UserRepository
|
||||
bookingRepo repositories.BookingRepository
|
||||
scheduleRepo repositories.ScheduleRepository
|
||||
}
|
||||
|
||||
// NewAdminService creates a new instance of AdminService
|
||||
func NewAdminService(
|
||||
userRepo repositories.UserRepository,
|
||||
bookingRepo repositories.BookingRepository,
|
||||
scheduleRepo repositories.ScheduleRepository,
|
||||
) AdminService {
|
||||
return &adminService{
|
||||
userRepo: userRepo,
|
||||
bookingRepo: bookingRepo,
|
||||
scheduleRepo: scheduleRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// GetDashboardStats retrieves dashboard statistics for admin overview
|
||||
func (s *adminService) GetDashboardStats() (*DashboardStats, error) {
|
||||
var stats DashboardStats
|
||||
|
||||
// Get user statistics
|
||||
totalUsers, err := s.userRepo.GetActiveUsersCount()
|
||||
if err != nil {
|
||||
log.Printf("Failed to get total users count: %v", err)
|
||||
return nil, fmt.Errorf("failed to get user statistics: %w", err)
|
||||
}
|
||||
stats.TotalUsers = totalUsers
|
||||
stats.ActiveUsers = totalUsers // For now, all users are considered active
|
||||
|
||||
// Get booking statistics
|
||||
bookingStats, err := s.bookingRepo.GetBookingStats()
|
||||
if err != nil {
|
||||
log.Printf("Failed to get booking statistics: %v", err)
|
||||
return nil, fmt.Errorf("failed to get booking statistics: %w", err)
|
||||
}
|
||||
stats.TotalBookings = bookingStats.TotalBookings
|
||||
stats.UpcomingBookings = bookingStats.UpcomingBookings
|
||||
stats.CompletedBookings = bookingStats.CompletedBookings
|
||||
stats.CancelledBookings = bookingStats.CancelledBookings
|
||||
|
||||
// Get financial statistics (all time)
|
||||
allTimeStart := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
allTimeEnd := time.Now()
|
||||
|
||||
financialStats, err := s.bookingRepo.GetFinancialStats(allTimeStart, allTimeEnd)
|
||||
if err != nil {
|
||||
log.Printf("Failed to get financial statistics: %v", err)
|
||||
return nil, fmt.Errorf("failed to get financial statistics: %w", err)
|
||||
}
|
||||
stats.TotalRevenue = financialStats.TotalRevenue
|
||||
|
||||
// Get monthly revenue (current month)
|
||||
now := time.Now()
|
||||
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
||||
monthEnd := monthStart.AddDate(0, 1, 0)
|
||||
|
||||
monthlyStats, err := s.bookingRepo.GetFinancialStats(monthStart, monthEnd)
|
||||
if err != nil {
|
||||
log.Printf("Failed to get monthly financial statistics: %v", err)
|
||||
return nil, fmt.Errorf("failed to get monthly financial statistics: %w", err)
|
||||
}
|
||||
stats.MonthlyRevenue = monthlyStats.TotalRevenue
|
||||
|
||||
return &stats, nil
|
||||
}
|
||||
|
||||
// GetFinancialReports generates financial reports for a given date range
|
||||
func (s *adminService) GetFinancialReports(startDate, endDate time.Time) (*FinancialReport, error) {
|
||||
// Validate date range
|
||||
if endDate.Before(startDate) {
|
||||
return nil, fmt.Errorf("end date cannot be before start date")
|
||||
}
|
||||
|
||||
// Get overall financial statistics
|
||||
financialStats, err := s.bookingRepo.GetFinancialStats(startDate, endDate)
|
||||
if err != nil {
|
||||
log.Printf("Failed to get financial statistics for date range %v to %v: %v", startDate, endDate, err)
|
||||
return nil, fmt.Errorf("failed to get financial statistics: %w", err)
|
||||
}
|
||||
|
||||
report := &FinancialReport{
|
||||
StartDate: startDate,
|
||||
EndDate: endDate,
|
||||
TotalRevenue: financialStats.TotalRevenue,
|
||||
TotalBookings: financialStats.TotalBookings,
|
||||
AverageBooking: financialStats.AverageBooking,
|
||||
}
|
||||
|
||||
// Generate daily breakdown
|
||||
dailyBreakdown, err := s.generateDailyBreakdown(startDate, endDate)
|
||||
if err != nil {
|
||||
log.Printf("Failed to generate daily breakdown: %v", err)
|
||||
return nil, fmt.Errorf("failed to generate daily breakdown: %w", err)
|
||||
}
|
||||
report.DailyBreakdown = dailyBreakdown
|
||||
|
||||
// Generate monthly breakdown
|
||||
monthlyBreakdown, err := s.generateMonthlyBreakdown(startDate, endDate)
|
||||
if err != nil {
|
||||
log.Printf("Failed to generate monthly breakdown: %v", err)
|
||||
return nil, fmt.Errorf("failed to generate monthly breakdown: %w", err)
|
||||
}
|
||||
report.MonthlyBreakdown = monthlyBreakdown
|
||||
|
||||
return report, nil
|
||||
}
|
||||
|
||||
// CreateSchedule creates a new schedule slot
|
||||
func (s *adminService) CreateSchedule(req CreateScheduleRequest) (*models.Schedule, error) {
|
||||
// Validate the request
|
||||
if req.EndTime.Before(req.StartTime) || req.EndTime.Equal(req.StartTime) {
|
||||
return nil, fmt.Errorf("end time must be after start time")
|
||||
}
|
||||
|
||||
if req.StartTime.Before(time.Now()) {
|
||||
return nil, fmt.Errorf("cannot create schedule slots in the past")
|
||||
}
|
||||
|
||||
if req.MaxBookings < 1 {
|
||||
return nil, fmt.Errorf("max bookings must be at least 1")
|
||||
}
|
||||
|
||||
// Create the schedule
|
||||
schedule := &models.Schedule{
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
MaxBookings: req.MaxBookings,
|
||||
IsAvailable: true,
|
||||
BookedCount: 0,
|
||||
}
|
||||
|
||||
if err := s.scheduleRepo.Create(schedule); err != nil {
|
||||
log.Printf("Failed to create schedule: %v", err)
|
||||
return nil, fmt.Errorf("failed to create schedule: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Successfully created schedule slot from %v to %v with max bookings %d",
|
||||
schedule.StartTime, schedule.EndTime, schedule.MaxBookings)
|
||||
|
||||
return schedule, nil
|
||||
}
|
||||
|
||||
// UpdateSchedule updates an existing schedule slot
|
||||
func (s *adminService) UpdateSchedule(scheduleID uint, req UpdateScheduleRequest) (*models.Schedule, error) {
|
||||
// Get the existing schedule
|
||||
schedule, err := s.scheduleRepo.GetByID(scheduleID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to get schedule %d: %v", scheduleID, err)
|
||||
return nil, fmt.Errorf("schedule not found: %w", err)
|
||||
}
|
||||
|
||||
// Update fields if provided
|
||||
if req.StartTime != nil {
|
||||
schedule.StartTime = *req.StartTime
|
||||
}
|
||||
if req.EndTime != nil {
|
||||
schedule.EndTime = *req.EndTime
|
||||
}
|
||||
if req.MaxBookings != nil {
|
||||
if *req.MaxBookings < 1 {
|
||||
return nil, fmt.Errorf("max bookings must be at least 1")
|
||||
}
|
||||
if *req.MaxBookings < schedule.BookedCount {
|
||||
return nil, fmt.Errorf("max bookings cannot be less than current booked count (%d)", schedule.BookedCount)
|
||||
}
|
||||
schedule.MaxBookings = *req.MaxBookings
|
||||
}
|
||||
if req.IsAvailable != nil {
|
||||
schedule.IsAvailable = *req.IsAvailable
|
||||
}
|
||||
|
||||
// Validate time range if updated
|
||||
if !schedule.EndTime.After(schedule.StartTime) {
|
||||
return nil, fmt.Errorf("end time must be after start time")
|
||||
}
|
||||
|
||||
// Update the schedule
|
||||
if err := s.scheduleRepo.Update(schedule); err != nil {
|
||||
log.Printf("Failed to update schedule %d: %v", scheduleID, err)
|
||||
return nil, fmt.Errorf("failed to update schedule: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Successfully updated schedule %d", scheduleID)
|
||||
|
||||
return schedule, nil
|
||||
}
|
||||
|
||||
// GetAllUsers retrieves all users with pagination
|
||||
func (s *adminService) GetAllUsers(limit, offset int) ([]models.User, int64, error) {
|
||||
if limit <= 0 {
|
||||
limit = 50 // Default limit
|
||||
}
|
||||
if limit > 100 {
|
||||
limit = 100 // Maximum limit
|
||||
}
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
users, total, err := s.userRepo.GetAllUsers(limit, offset)
|
||||
if err != nil {
|
||||
log.Printf("Failed to get users with limit %d, offset %d: %v", limit, offset, err)
|
||||
return nil, 0, fmt.Errorf("failed to get users: %w", err)
|
||||
}
|
||||
|
||||
return users, total, nil
|
||||
}
|
||||
|
||||
// GetAllBookings retrieves all bookings with pagination
|
||||
func (s *adminService) GetAllBookings(limit, offset int) ([]models.Booking, int64, error) {
|
||||
if limit <= 0 {
|
||||
limit = 50 // Default limit
|
||||
}
|
||||
if limit > 100 {
|
||||
limit = 100 // Maximum limit
|
||||
}
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
bookings, total, err := s.bookingRepo.GetAllBookings(limit, offset)
|
||||
if err != nil {
|
||||
log.Printf("Failed to get bookings with limit %d, offset %d: %v", limit, offset, err)
|
||||
return nil, 0, fmt.Errorf("failed to get bookings: %w", err)
|
||||
}
|
||||
|
||||
return bookings, total, nil
|
||||
}
|
||||
|
||||
// generateDailyBreakdown generates daily financial breakdown for the given date range
|
||||
func (s *adminService) generateDailyBreakdown(startDate, endDate time.Time) ([]DailyFinancialSummary, error) {
|
||||
var breakdown []DailyFinancialSummary
|
||||
|
||||
// Iterate through each day in the range
|
||||
for d := startDate; !d.After(endDate); d = d.AddDate(0, 0, 1) {
|
||||
dayStart := time.Date(d.Year(), d.Month(), d.Day(), 0, 0, 0, 0, d.Location())
|
||||
dayEnd := dayStart.AddDate(0, 0, 1)
|
||||
|
||||
stats, err := s.bookingRepo.GetFinancialStats(dayStart, dayEnd)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get daily stats for %v: %w", d, err)
|
||||
}
|
||||
|
||||
breakdown = append(breakdown, DailyFinancialSummary{
|
||||
Date: dayStart,
|
||||
Revenue: stats.TotalRevenue,
|
||||
Bookings: stats.TotalBookings,
|
||||
})
|
||||
}
|
||||
|
||||
return breakdown, nil
|
||||
}
|
||||
|
||||
// generateMonthlyBreakdown generates monthly financial breakdown for the given date range
|
||||
func (s *adminService) generateMonthlyBreakdown(startDate, endDate time.Time) ([]MonthlyFinancialSummary, error) {
|
||||
var breakdown []MonthlyFinancialSummary
|
||||
|
||||
// Start from the beginning of the start month
|
||||
current := time.Date(startDate.Year(), startDate.Month(), 1, 0, 0, 0, 0, startDate.Location())
|
||||
endMonth := time.Date(endDate.Year(), endDate.Month(), 1, 0, 0, 0, 0, endDate.Location())
|
||||
|
||||
for !current.After(endMonth) {
|
||||
monthStart := current
|
||||
monthEnd := monthStart.AddDate(0, 1, 0)
|
||||
|
||||
// Adjust for the actual date range
|
||||
if monthStart.Before(startDate) {
|
||||
monthStart = startDate
|
||||
}
|
||||
if monthEnd.After(endDate) {
|
||||
monthEnd = endDate
|
||||
}
|
||||
|
||||
stats, err := s.bookingRepo.GetFinancialStats(monthStart, monthEnd)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get monthly stats for %v: %w", current, err)
|
||||
}
|
||||
|
||||
breakdown = append(breakdown, MonthlyFinancialSummary{
|
||||
Month: current.Format("2006-01"),
|
||||
Revenue: stats.TotalRevenue,
|
||||
Bookings: stats.TotalBookings,
|
||||
})
|
||||
|
||||
current = current.AddDate(0, 1, 0)
|
||||
}
|
||||
|
||||
return breakdown, nil
|
||||
}
|
||||
@ -49,6 +49,16 @@ type JitsiService interface {
|
||||
DeleteMeeting(roomID string) error
|
||||
}
|
||||
|
||||
// AdminService handles admin dashboard operations
|
||||
type AdminService interface {
|
||||
GetDashboardStats() (*DashboardStats, error)
|
||||
GetFinancialReports(startDate, endDate time.Time) (*FinancialReport, error)
|
||||
CreateSchedule(req CreateScheduleRequest) (*models.Schedule, error)
|
||||
UpdateSchedule(scheduleID uint, req UpdateScheduleRequest) (*models.Schedule, error)
|
||||
GetAllUsers(limit, offset int) ([]models.User, int64, error)
|
||||
GetAllBookings(limit, offset int) ([]models.Booking, int64, error)
|
||||
}
|
||||
|
||||
// JitsiMeeting represents a Jitsi meeting
|
||||
type JitsiMeeting struct {
|
||||
RoomID string `json:"room_id"`
|
||||
@ -89,3 +99,50 @@ type CreatePaymentIntentRequest struct {
|
||||
type ConfirmPaymentRequest struct {
|
||||
PaymentIntentID string `json:"payment_intent_id" binding:"required"`
|
||||
}
|
||||
|
||||
// Admin DTOs
|
||||
type DashboardStats struct {
|
||||
TotalUsers int64 `json:"total_users"`
|
||||
ActiveUsers int64 `json:"active_users"`
|
||||
TotalBookings int64 `json:"total_bookings"`
|
||||
UpcomingBookings int64 `json:"upcoming_bookings"`
|
||||
CompletedBookings int64 `json:"completed_bookings"`
|
||||
CancelledBookings int64 `json:"cancelled_bookings"`
|
||||
TotalRevenue float64 `json:"total_revenue"`
|
||||
MonthlyRevenue float64 `json:"monthly_revenue"`
|
||||
}
|
||||
|
||||
type FinancialReport struct {
|
||||
StartDate time.Time `json:"start_date"`
|
||||
EndDate time.Time `json:"end_date"`
|
||||
TotalRevenue float64 `json:"total_revenue"`
|
||||
TotalBookings int64 `json:"total_bookings"`
|
||||
AverageBooking float64 `json:"average_booking"`
|
||||
DailyBreakdown []DailyFinancialSummary `json:"daily_breakdown"`
|
||||
MonthlyBreakdown []MonthlyFinancialSummary `json:"monthly_breakdown"`
|
||||
}
|
||||
|
||||
type DailyFinancialSummary struct {
|
||||
Date time.Time `json:"date"`
|
||||
Revenue float64 `json:"revenue"`
|
||||
Bookings int64 `json:"bookings"`
|
||||
}
|
||||
|
||||
type MonthlyFinancialSummary struct {
|
||||
Month string `json:"month"`
|
||||
Revenue float64 `json:"revenue"`
|
||||
Bookings int64 `json:"bookings"`
|
||||
}
|
||||
|
||||
type CreateScheduleRequest struct {
|
||||
StartTime time.Time `json:"start_time" binding:"required"`
|
||||
EndTime time.Time `json:"end_time" binding:"required"`
|
||||
MaxBookings int `json:"max_bookings" binding:"required,min=1"`
|
||||
}
|
||||
|
||||
type UpdateScheduleRequest struct {
|
||||
StartTime *time.Time `json:"start_time"`
|
||||
EndTime *time.Time `json:"end_time"`
|
||||
MaxBookings *int `json:"max_bookings"`
|
||||
IsAvailable *bool `json:"is_available"`
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user