package handlers import ( "fmt" "net/http" "strconv" "time" "attune-heart-therapy/internal/middleware" "attune-heart-therapy/internal/models" "attune-heart-therapy/internal/repositories" "attune-heart-therapy/internal/services" "github.com/gin-gonic/gin" ) type AdminHandler struct { adminService services.AdminService userRepo repositories.UserRepository jitsiService services.JitsiService } func NewAdminHandler(adminService services.AdminService, userRepo repositories.UserRepository, jitsiService services.JitsiService) *AdminHandler { return &AdminHandler{ adminService: adminService, userRepo: userRepo, jitsiService: jitsiService, } } // AdminBookingResponse represents a booking with admin's personalized meeting link type AdminBookingResponse struct { models.Booking AdminMeetingURL string `json:"admin_meeting_url,omitempty"` } // addAdminLink adds an admin's personalized meeting link to a booking func (h *AdminHandler) addAdminLink(booking *models.Booking, admin *models.User) AdminBookingResponse { response := AdminBookingResponse{ Booking: *booking, } // Only add personalized link if Jitsi URL exists if booking.JitsiRoomURL != "" && booking.JitsiRoomID != "" { displayName := fmt.Sprintf("%s %s", admin.FirstName, admin.LastName) if displayName == " " || displayName == "" { displayName = admin.Email } // Generate JWT token for admin with moderator privileges token, err := h.jitsiService.GenerateJitsiToken( booking.JitsiRoomID, displayName, admin.Email, true, // Always true for admin ) var adminURL string if err == nil && token != "" { // Use JWT token if available adminURL = fmt.Sprintf("%s?jwt=%s", booking.JitsiRoomURL, token) } else { // Fallback to URL parameters adminURL = fmt.Sprintf("%s#userInfo.displayName=\"%s\"&config.startWithAudioMuted=false&config.startWithVideoMuted=false", booking.JitsiRoomURL, displayName) } response.AdminMeetingURL = adminURL } return response } // GetDashboard handles GET /api/admin/dashboard for dashboard statistics func (h *AdminHandler) GetDashboard(c *gin.Context) { 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) { 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) { // 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) { // Get admin user ID from JWT token adminID, exists := middleware.GetUserIDFromContext(c) if !exists { c.JSON(http.StatusUnauthorized, gin.H{ "error": "User not authenticated", }) return } // Get admin user details admin, err := h.userRepo.GetByID(adminID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to get admin information", "details": err.Error(), }) return } // 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 } // Add admin's personalized meeting links to each booking bookingResponses := make([]AdminBookingResponse, len(bookings)) for i, booking := range bookings { bookingResponses[i] = h.addAdminLink(&booking, admin) } c.JSON(http.StatusOK, gin.H{ "bookings": bookingResponses, "total": total, "limit": limit, "offset": offset, }) } // GetFinancialReports handles GET /api/admin/reports/financial for financial reports func (h *AdminHandler) GetFinancialReports(c *gin.Context) { // 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, }) }