package today import ( "database/sql" "encoding/json" "log" "net/http" "time" "crussell/db" ) type ServiceInfo struct { ServiceName *string `json:"service_name,omitempty"` ServiceDescription *string `json:"service_description,omitempty"` Price *float64 `json:"price,omitempty"` DurationMinutes *int `json:"duration_minutes,omitempty"` } type UserInfo struct { ID string `json:"id"` FullName string `json:"full_name"` Phone *string `json:"phone,omitempty"` Email *string `json:"email,omitempty"` ProfilePicURL *string `json:"profile_pic_url,omitempty"` } type AppointmentInfo struct { ID string `json:"id"` StartTime time.Time `json:"start_time"` Status string `json:"status"` Notes *string `json:"notes,omitempty"` User *UserInfo `json:"user,omitempty"` Services []ServiceInfo `json:"services"` DurationMinutes int `json:"duration_minutes"` TotalAmount float64 `json:"total_amount"` } type CurrentNextResponse struct { Current *AppointmentInfo `json:"current"` Next *AppointmentInfo `json:"next"` } // GET /api/admin/today/current-next func GetCurrentAndNextHandler(w http.ResponseWriter, r *http.Request) { now := time.Now() // Auto-transition confirmed bookings that have started but not ended to in_progress _, err := db.DB.Exec(r.Context(), ` UPDATE bookings SET status = 'in_progress' WHERE status = 'confirmed' AND start_time <= $1 AND ( start_time + ( COALESCE( (SELECT SUM(COALESCE(bs.override_duration_minutes, s.duration_minutes)) FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = bookings.id), 0 ) || ' minutes' )::interval ) > $1 `, now) if err != nil { log.Printf("Failed to auto-transition bookings to in_progress: %v", err) } // Auto-transition in_progress bookings that have ended to completed _, err = db.DB.Exec(r.Context(), ` UPDATE bookings SET status = 'completed' WHERE status = 'in_progress' AND ( start_time + ( COALESCE( (SELECT SUM(COALESCE(bs.override_duration_minutes, s.duration_minutes)) FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = bookings.id), 0 ) || ' minutes' )::interval ) <= $1 `, now) if err != nil { log.Printf("Failed to auto-transition bookings to completed: %v", err) } var current *AppointmentInfo var next *AppointmentInfo // Step 1: Try to find current in-progress appointment currentBooking, err := fetchAppointment(r, ` SELECT b.id, b.start_time, b.status, b.notes, b.user_id FROM bookings b WHERE b.status = 'in_progress' ORDER BY b.start_time ASC LIMIT 1 `) if err != nil && err != sql.ErrNoRows { log.Printf("Error querying current appointment: %v", err) // Don't fail, continue to find next } if err == nil && currentBooking != nil { current = currentBooking // Also fetch the appointment AFTER this one for free time calculation nextBooking, err := fetchAppointment(r, ` SELECT b.id, b.start_time, b.status, b.notes, b.user_id FROM bookings b WHERE b.start_time > $1 AND (b.status = 'confirmed' OR b.status = 'pending' OR b.status = 'in_progress') ORDER BY b.start_time ASC LIMIT 1 `, currentBooking.StartTime) if err == nil && nextBooking != nil { next = nextBooking } } else { // No current in-progress appointment, find the next upcoming one nextBooking, err := fetchAppointment(r, ` SELECT b.id, b.start_time, b.status, b.notes, b.user_id FROM bookings b WHERE b.start_time >= $1 AND (b.status = 'confirmed' OR b.status = 'pending') ORDER BY b.start_time ASC LIMIT 1 `, now) if err != nil && err != sql.ErrNoRows { log.Printf("Error querying next appointment: %v", err) } if err == nil && nextBooking != nil { current = nextBooking // Fetch the one after for free time calculation afterNext, err := fetchAppointment(r, ` SELECT b.id, b.start_time, b.status, b.notes, b.user_id FROM bookings b WHERE b.start_time > $1 AND (b.status = 'confirmed' OR b.status = 'pending' OR b.status = 'in_progress') ORDER BY b.start_time ASC LIMIT 1 `, nextBooking.StartTime) if err == nil && afterNext != nil { next = afterNext } } } response := CurrentNextResponse{ Current: current, Next: next, } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(response); err != nil { log.Printf("Failed to encode current/next response: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } } // Helper function to fetch a single appointment with all details func fetchAppointment(r *http.Request, query string, args ...interface{}) (*AppointmentInfo, error) { var bookingID string var startTime time.Time var status string var notes sql.NullString var userID string err := db.DB.QueryRow(r.Context(), query, args...).Scan( &bookingID, &startTime, &status, ¬es, &userID, ) if err != nil { return nil, err } appointment := &AppointmentInfo{ ID: bookingID, StartTime: startTime, Status: status, } if notes.Valid { appointment.Notes = ¬es.String } // Fetch user info var user UserInfo var phone sql.NullString var email sql.NullString var profilePicURL sql.NullString err = db.DB.QueryRow(r.Context(), ` SELECT id, fn, phone, email, profile_pic_url FROM users WHERE id = $1 `, userID).Scan(&user.ID, &user.FullName, &phone, &email, &profilePicURL) if err == nil { if phone.Valid { user.Phone = &phone.String } if profilePicURL.Valid { user.ProfilePicURL = &profilePicURL.String } if email.Valid { user.Email = &email.String } appointment.User = &user } else { log.Printf("Failed to fetch user %s: %v", userID, err) } // Fetch services serviceRows, err := db.DB.Query(r.Context(), ` SELECT s.name, s.description, COALESCE(bs.override_price, s.price) as price, COALESCE(bs.override_duration_minutes, s.duration_minutes) as duration_minutes FROM booking_services bs LEFT JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = $1 ORDER BY s.name `, bookingID) if err != nil { log.Printf("Failed to fetch services for booking %s: %v", bookingID, err) return appointment, nil } defer serviceRows.Close() var totalAmount float64 var totalDuration int for serviceRows.Next() { var service ServiceInfo var name string var description sql.NullString var price float64 var duration int if err := serviceRows.Scan(&name, &description, &price, &duration); err != nil { log.Printf("Failed to scan service: %v", err) continue } service.ServiceName = &name if description.Valid { service.ServiceDescription = &description.String } service.Price = &price service.DurationMinutes = &duration totalAmount += price totalDuration += duration appointment.Services = append(appointment.Services, service) } appointment.TotalAmount = totalAmount appointment.DurationMinutes = totalDuration return appointment, nil } type TodayAppointment struct { ID string `json:"id"` StartTime string `json:"start_time"` Status string `json:"status"` UserName string `json:"user_name"` UserID string `json:"user_id"` Services []string `json:"services"` DurationMinutes int `json:"duration_minutes"` } type TodayAppointmentsResponse struct { Appointments []TodayAppointment `json:"appointments"` } // GET /api/admin/today/appointments func GetTodayAppointmentsHandler(w http.ResponseWriter, r *http.Request) { now := time.Now() // Auto-transition confirmed bookings that have started but not ended to in_progress _, err := db.DB.Exec(r.Context(), ` UPDATE bookings SET status = 'in_progress' WHERE status = 'confirmed' AND start_time <= $1 AND ( start_time + ( COALESCE( (SELECT SUM(COALESCE(bs.override_duration_minutes, s.duration_minutes)) FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = bookings.id), 0 ) || ' minutes' )::interval ) > $1 `, now) if err != nil { log.Printf("Failed to auto-transition bookings to in_progress: %v", err) } // Auto-transition in_progress bookings that have ended to completed _, err = db.DB.Exec(r.Context(), ` UPDATE bookings SET status = 'completed' WHERE status = 'in_progress' AND ( start_time + ( COALESCE( (SELECT SUM(COALESCE(bs.override_duration_minutes, s.duration_minutes)) FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = bookings.id), 0 ) || ' minutes' )::interval ) <= $1 `, now) if err != nil { log.Printf("Failed to auto-transition bookings to completed: %v", err) } todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) todayEnd := todayStart.Add(24 * time.Hour) // Fetch all bookings for today rows, err := db.DB.Query(r.Context(), ` SELECT b.id, b.start_time, b.status, u.fn as user_name, u.id as user_id FROM bookings b LEFT JOIN users u ON b.user_id = u.id WHERE b.start_time >= $1 AND b.start_time < $2 ORDER BY b.start_time ASC `, todayStart, todayEnd) if err != nil { log.Printf("Failed to fetch today's appointments: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } defer rows.Close() var appointments []TodayAppointment for rows.Next() { var apt TodayAppointment var startTime time.Time err := rows.Scan( &apt.ID, &startTime, &apt.Status, &apt.UserName, &apt.UserID, ) if err != nil { log.Printf("Failed to scan appointment row: %v", err) continue } apt.StartTime = startTime.Format(time.RFC3339) // Fetch services for this booking serviceRows, err := db.DB.Query(r.Context(), ` SELECT s.name, COALESCE(bs.override_duration_minutes, s.duration_minutes) as duration_minutes FROM booking_services bs LEFT JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = $1 ORDER BY s.name `, apt.ID) if err != nil { log.Printf("Failed to fetch services for booking %s: %v", apt.ID, err) continue } var services []string var totalDuration int for serviceRows.Next() { var serviceName string var duration int if err := serviceRows.Scan(&serviceName, &duration); err != nil { log.Printf("Failed to scan service: %v", err) continue } services = append(services, serviceName) totalDuration += duration } serviceRows.Close() apt.Services = services apt.DurationMinutes = totalDuration appointments = append(appointments, apt) } if appointments == nil { appointments = []TodayAppointment{} } response := TodayAppointmentsResponse{ Appointments: appointments, } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(response); err != nil { log.Printf("Failed to encode today's appointments response: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } } type PendingApproval struct { ID string `json:"id"` StartTime string `json:"start_time"` UserID string `json:"user_id"` UserName string `json:"user_name"` Services []string `json:"services"` DurationMinutes int `json:"duration_minutes"` CreatedAt string `json:"created_at"` } type PendingApprovalsResponse struct { Approvals []PendingApproval `json:"approvals"` } // GET /api/admin/today/pending-approvals func GetPendingApprovalsHandler(w http.ResponseWriter, r *http.Request) { rows, err := db.DB.Query(r.Context(), ` SELECT b.id, b.start_time, b.created_at, u.id as user_id, u.fn as user_name FROM bookings b LEFT JOIN users u ON b.user_id = u.id WHERE b.status = 'pending' ORDER BY b.created_at ASC `) if err != nil { log.Printf("Failed to fetch pending approvals: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } defer rows.Close() var approvals []PendingApproval for rows.Next() { var apt PendingApproval var startTime time.Time var createdAt time.Time err := rows.Scan( &apt.ID, &startTime, &createdAt, &apt.UserID, &apt.UserName, ) if err != nil { log.Printf("Failed to scan pending approval row: %v", err) continue } apt.StartTime = startTime.Format(time.RFC3339) apt.CreatedAt = createdAt.Format(time.RFC3339) // Fetch services serviceRows, err := db.DB.Query(r.Context(), ` SELECT s.name, COALESCE(bs.override_duration_minutes, s.duration_minutes) as duration_minutes FROM booking_services bs LEFT JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = $1 ORDER BY s.name `, apt.ID) if err != nil { log.Printf("Failed to fetch services for booking %s: %v", apt.ID, err) continue } var services []string var totalDuration int for serviceRows.Next() { var name string var duration int if err := serviceRows.Scan(&name, &duration); err != nil { log.Printf("Failed to scan service: %v", err) continue } services = append(services, name) totalDuration += duration } serviceRows.Close() apt.Services = services apt.DurationMinutes = totalDuration approvals = append(approvals, apt) } if approvals == nil { approvals = []PendingApproval{} } response := PendingApprovalsResponse{ Approvals: approvals, } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(response); err != nil { log.Printf("Failed to encode pending approvals response: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } }