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
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"attune-heart-therapy/internal/services"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AdminHandler struct {
|
type AdminHandler struct {
|
||||||
// Will be implemented in later tasks
|
adminService services.AdminService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAdminHandler() *AdminHandler {
|
func NewAdminHandler(adminService services.AdminService) *AdminHandler {
|
||||||
return &AdminHandler{}
|
return &AdminHandler{
|
||||||
|
adminService: adminService,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDashboard handles GET /api/admin/dashboard for dashboard statistics
|
||||||
func (h *AdminHandler) GetDashboard(c *gin.Context) {
|
func (h *AdminHandler) GetDashboard(c *gin.Context) {
|
||||||
// Will be implemented in task 11
|
stats, err := h.adminService.GetDashboardStats()
|
||||||
c.JSON(501, gin.H{"message": "Not implemented yet"})
|
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) {
|
func (h *AdminHandler) CreateSchedule(c *gin.Context) {
|
||||||
// Will be implemented in task 11
|
var req services.CreateScheduleRequest
|
||||||
c.JSON(501, gin.H{"message": "Not implemented yet"})
|
|
||||||
|
// 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) {
|
func (h *AdminHandler) GetUsers(c *gin.Context) {
|
||||||
// Will be implemented in task 11
|
// Parse pagination parameters
|
||||||
c.JSON(501, gin.H{"message": "Not implemented yet"})
|
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) {
|
func (h *AdminHandler) GetBookings(c *gin.Context) {
|
||||||
// Will be implemented in task 11
|
// Parse pagination parameters
|
||||||
c.JSON(501, gin.H{"message": "Not implemented yet"})
|
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) {
|
func (h *AdminHandler) GetFinancialReports(c *gin.Context) {
|
||||||
// Will be implemented in task 11
|
// Parse date parameters
|
||||||
c.JSON(501, gin.H{"message": "Not implemented yet"})
|
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
|
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"
|
"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
|
// UserRepository handles user data persistence
|
||||||
type UserRepository interface {
|
type UserRepository interface {
|
||||||
Create(user *models.User) error
|
Create(user *models.User) error
|
||||||
@ -13,6 +28,7 @@ type UserRepository interface {
|
|||||||
GetByEmail(email string) (*models.User, error)
|
GetByEmail(email string) (*models.User, error)
|
||||||
Update(user *models.User) error
|
Update(user *models.User) error
|
||||||
GetActiveUsersCount() (int64, error)
|
GetActiveUsersCount() (int64, error)
|
||||||
|
GetAllUsers(limit, offset int) ([]models.User, int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BookingRepository handles booking data persistence
|
// BookingRepository handles booking data persistence
|
||||||
@ -24,6 +40,9 @@ type BookingRepository interface {
|
|||||||
Update(booking *models.Booking) error
|
Update(booking *models.Booking) error
|
||||||
Delete(id uint) error
|
Delete(id uint) error
|
||||||
GetUpcomingBookings() ([]models.Booking, 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
|
// ScheduleRepository handles schedule data persistence
|
||||||
|
|||||||
@ -110,3 +110,26 @@ func (r *userRepository) GetActiveUsersCount() (int64, error) {
|
|||||||
|
|
||||||
return count, nil
|
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
|
router *gin.Engine
|
||||||
paymentHandler *handlers.PaymentHandler
|
paymentHandler *handlers.PaymentHandler
|
||||||
bookingHandler *handlers.BookingHandler
|
bookingHandler *handlers.BookingHandler
|
||||||
|
adminHandler *handlers.AdminHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(cfg *config.Config) *Server {
|
func New(cfg *config.Config) *Server {
|
||||||
@ -126,12 +127,16 @@ func (s *Server) setupRoutes() {
|
|||||||
payments.POST("/webhook", s.paymentHandler.HandleWebhook)
|
payments.POST("/webhook", s.paymentHandler.HandleWebhook)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Admin routes (will be implemented in later tasks)
|
// Admin routes - require admin authentication
|
||||||
admin := v1.Group("/admin")
|
admin := v1.Group("/admin")
|
||||||
|
// Note: Admin authentication middleware will be added in task 13
|
||||||
{
|
{
|
||||||
admin.GET("/dashboard", func(c *gin.Context) {
|
admin.GET("/dashboard", s.adminHandler.GetDashboard)
|
||||||
c.JSON(501, gin.H{"message": "Not implemented yet"})
|
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,
|
notificationService,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Initialize admin service
|
||||||
|
adminService := services.NewAdminService(repos.User, repos.Booking, repos.Schedule)
|
||||||
|
|
||||||
// Initialize handlers
|
// Initialize handlers
|
||||||
s.paymentHandler = handlers.NewPaymentHandler(paymentService)
|
s.paymentHandler = handlers.NewPaymentHandler(paymentService)
|
||||||
s.bookingHandler = handlers.NewBookingHandler(bookingService)
|
s.bookingHandler = handlers.NewBookingHandler(bookingService)
|
||||||
|
s.adminHandler = handlers.NewAdminHandler(adminService)
|
||||||
}
|
}
|
||||||
|
|
||||||
// healthCheck handles the health check endpoint
|
// 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
|
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
|
// JitsiMeeting represents a Jitsi meeting
|
||||||
type JitsiMeeting struct {
|
type JitsiMeeting struct {
|
||||||
RoomID string `json:"room_id"`
|
RoomID string `json:"room_id"`
|
||||||
@ -89,3 +99,50 @@ type CreatePaymentIntentRequest struct {
|
|||||||
type ConfirmPaymentRequest struct {
|
type ConfirmPaymentRequest struct {
|
||||||
PaymentIntentID string `json:"payment_intent_id" binding:"required"`
|
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