added booking approvals
This commit is contained in:
@@ -0,0 +1,464 @@
|
||||
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()
|
||||
|
||||
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()
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user