From c265e8f866d559f8cdc51c1b77c987abc9c0b9a5 Mon Sep 17 00:00:00 2001 From: ats-tech25 Date: Wed, 5 Nov 2025 16:58:34 +0000 Subject: [PATCH] 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 --- internal/handlers/admin.go | 251 +++++++++++++++- internal/repositories/booking_repository.go | 86 ++++++ internal/repositories/interfaces.go | 19 ++ internal/repositories/user_repository.go | 23 ++ internal/server/server.go | 17 +- internal/services/admin_service.go | 304 ++++++++++++++++++++ internal/services/interfaces.go | 57 ++++ 7 files changed, 740 insertions(+), 17 deletions(-) create mode 100644 internal/services/admin_service.go diff --git a/internal/handlers/admin.go b/internal/handlers/admin.go index 357aa9f..6ba0d3a 100644 --- a/internal/handlers/admin.go +++ b/internal/handlers/admin.go @@ -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, + }) } diff --git a/internal/repositories/booking_repository.go b/internal/repositories/booking_repository.go index 05cd0f9..2391584 100644 --- a/internal/repositories/booking_repository.go +++ b/internal/repositories/booking_repository.go @@ -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 +} diff --git a/internal/repositories/interfaces.go b/internal/repositories/interfaces.go index bd58457..d15c065 100644 --- a/internal/repositories/interfaces.go +++ b/internal/repositories/interfaces.go @@ -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 diff --git a/internal/repositories/user_repository.go b/internal/repositories/user_repository.go index 66c8d0d..6a2da47 100644 --- a/internal/repositories/user_repository.go +++ b/internal/repositories/user_repository.go @@ -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 +} diff --git a/internal/server/server.go b/internal/server/server.go index f257e84..88e305a 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -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 diff --git a/internal/services/admin_service.go b/internal/services/admin_service.go new file mode 100644 index 0000000..b522802 --- /dev/null +++ b/internal/services/admin_service.go @@ -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 +} diff --git a/internal/services/interfaces.go b/internal/services/interfaces.go index c5b8846..0464648 100644 --- a/internal/services/interfaces.go +++ b/internal/services/interfaces.go @@ -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"` +}