Add detailed API: - Complete API documentation for In Format Usage flow diagrams for authentication and booking processes - Comprehensive endpoint descriptions with request/response examples - Detailed authentication and booking flow explanations - Structured documentation for health checks, authentication, and booking endpoints -: - Includes JWT authentication details usage - Provides clear API usage patterns for users and clients and administrators system interactions - Enhances project documentation with provides clear, structured API reference - Improves developer and user understanding of system capabilities
340 lines
8.5 KiB
Go
340 lines
8.5 KiB
Go
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,
|
|
})
|
|
}
|