feat: add staged default hours change handlers with conflict detection
Add ScheduleDefaultHoursChange, GetScheduledDefaultHoursChange, CancelScheduledDefaultHoursChange, and GetDefaultHoursConflictingBookings handlers. Updates GetDefaultHours to return { current, scheduled_change }. Updates GetWorkingHours and computeAvailableHours to apply staged hours for dates on/after the effective_date.
This commit is contained in:
@@ -45,6 +45,48 @@ type DayWorkingHours struct {
|
|||||||
Source string `json:"source"` // "default" or "exceptional"
|
Source string `json:"source"` // "default" or "exceptional"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ScheduledHoursChange represents a pending default hours change.
|
||||||
|
type ScheduledHoursChange struct {
|
||||||
|
EffectiveDate string `json:"effective_date"`
|
||||||
|
Hours []DefaultHours `json:"hours"`
|
||||||
|
CreatedAt string `json:"created_at,omitempty"`
|
||||||
|
CreatedBy string `json:"created_by,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScheduledHoursChangeResponse wraps current + scheduled hours.
|
||||||
|
type ScheduledHoursChangeResponse struct {
|
||||||
|
Current []DefaultHours `json:"current"`
|
||||||
|
ScheduledChange *ScheduledHoursChange `json:"scheduled_change,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadScheduledChange loads the pending default hours scheduled change, if any.
|
||||||
|
// Returns nil when no pending change exists.
|
||||||
|
func loadScheduledChange(ctx context.Context) *ScheduledHoursChange {
|
||||||
|
var effDate, createdAt, createdBy, hoursJSON string
|
||||||
|
err := db.Conn.QueryRow(ctx, `
|
||||||
|
SELECT effective_date::text,
|
||||||
|
COALESCE(created_at::text, ''),
|
||||||
|
COALESCE(created_by, ''),
|
||||||
|
hours::text
|
||||||
|
FROM default_hours_scheduled_changes
|
||||||
|
WHERE applied_at IS NULL AND cancelled_at IS NULL
|
||||||
|
LIMIT 1
|
||||||
|
`).Scan(&effDate, &createdAt, &createdBy, &hoursJSON)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var sh []DefaultHours
|
||||||
|
if json.Unmarshal([]byte(hoursJSON), &sh) != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &ScheduledHoursChange{
|
||||||
|
EffectiveDate: effDate,
|
||||||
|
Hours: sh,
|
||||||
|
CreatedAt: createdAt,
|
||||||
|
CreatedBy: createdBy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Default Hours Handlers ---
|
// --- Default Hours Handlers ---
|
||||||
func GetDefaultHours(w http.ResponseWriter, r *http.Request) {
|
func GetDefaultHours(w http.ResponseWriter, r *http.Request) {
|
||||||
rows, err := db.Conn.Query(r.Context(), `
|
rows, err := db.Conn.Query(r.Context(), `
|
||||||
@@ -52,6 +94,7 @@ func GetDefaultHours(w http.ResponseWriter, r *http.Request) {
|
|||||||
FROM working_hours ORDER BY weekday
|
FROM working_hours ORDER BY weekday
|
||||||
`)
|
`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Printf("Failed to fetch default hours: %v", err)
|
||||||
http.Error(w, "failed to fetch default hours", http.StatusInternalServerError)
|
http.Error(w, "failed to fetch default hours", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -61,18 +104,26 @@ func GetDefaultHours(w http.ResponseWriter, r *http.Request) {
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var h DefaultHours
|
var h DefaultHours
|
||||||
if err := rows.Scan(&h.Weekday, &h.StartTime, &h.EndTime, &h.IsOpen); err != nil {
|
if err := rows.Scan(&h.Weekday, &h.StartTime, &h.EndTime, &h.IsOpen); err != nil {
|
||||||
|
log.Printf("Failed to scan default hours: %v", err)
|
||||||
http.Error(w, "failed to scan default hours", http.StatusInternalServerError)
|
http.Error(w, "failed to scan default hours", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
hours = append(hours, h)
|
hours = append(hours, h)
|
||||||
}
|
}
|
||||||
if err := rows.Err(); err != nil {
|
if err := rows.Err(); err != nil {
|
||||||
http.Error(w, "error iterating default hours", http.StatusInternalServerError)
|
log.Printf("Error iterating default hours: %v", err)
|
||||||
|
http.Error(w, "error loading default hours", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for pending scheduled change
|
||||||
|
resp := ScheduledHoursChangeResponse{Current: hours}
|
||||||
|
if sc := loadScheduledChange(r.Context()); sc != nil {
|
||||||
|
resp.ScheduledChange = sc
|
||||||
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
if err := json.NewEncoder(w).Encode(hours); err != nil {
|
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||||
log.Printf("Failed to encode JSON response: %v", err)
|
log.Printf("Failed to encode JSON response: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -215,6 +266,9 @@ func GetWorkingHours(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load staged default hours change (if any)
|
||||||
|
stagedChange := loadScheduledChange(r.Context())
|
||||||
|
|
||||||
// Load exceptional applications for Mondays in range
|
// Load exceptional applications for Mondays in range
|
||||||
// Expand start to the Monday of its week so single-day queries still find the correct application
|
// Expand start to the Monday of its week so single-day queries still find the correct application
|
||||||
startWeekday := int(start.Weekday())
|
startWeekday := int(start.Weekday())
|
||||||
@@ -328,6 +382,19 @@ func GetWorkingHours(w http.ResponseWriter, r *http.Request) {
|
|||||||
day.EndTime = applied.EndTime
|
day.EndTime = applied.EndTime
|
||||||
day.IsOpen = applied.IsOpen
|
day.IsOpen = applied.IsOpen
|
||||||
day.Source = "exceptional"
|
day.Source = "exceptional"
|
||||||
|
} else if stagedChange != nil {
|
||||||
|
dayDateStr := d.Format("2006-01-02")
|
||||||
|
if dayDateStr >= stagedChange.EffectiveDate {
|
||||||
|
for _, sh := range stagedChange.Hours {
|
||||||
|
if sh.Weekday == weekday {
|
||||||
|
day.StartTime = sh.StartTime
|
||||||
|
day.EndTime = sh.EndTime
|
||||||
|
day.IsOpen = sh.IsOpen
|
||||||
|
day.Source = "default"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if def, ok := defaultMap[weekday]; ok {
|
} else if def, ok := defaultMap[weekday]; ok {
|
||||||
day.StartTime = def.StartTime
|
day.StartTime = def.StartTime
|
||||||
day.EndTime = def.EndTime
|
day.EndTime = def.EndTime
|
||||||
@@ -495,6 +562,9 @@ func computeAvailableHours(ctx context.Context, params availableHoursParams) ([]
|
|||||||
return nil, fmt.Errorf("error iterating default hours: %w", err)
|
return nil, fmt.Errorf("error iterating default hours: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load staged default hours change (if any)
|
||||||
|
stagedChange := loadScheduledChange(ctx)
|
||||||
|
|
||||||
// Load exceptional applications for Mondays in range
|
// Load exceptional applications for Mondays in range
|
||||||
startWeekday := int(start.Weekday())
|
startWeekday := int(start.Weekday())
|
||||||
daysSinceMonday := startWeekday - 1
|
daysSinceMonday := startWeekday - 1
|
||||||
@@ -702,6 +772,16 @@ func computeAvailableHours(ctx context.Context, params availableHoursParams) ([]
|
|||||||
baseEnd = applied.EndTime
|
baseEnd = applied.EndTime
|
||||||
isOpen = applied.IsOpen
|
isOpen = applied.IsOpen
|
||||||
day.Source = "exceptional"
|
day.Source = "exceptional"
|
||||||
|
} else if stagedChange != nil && d.Format("2006-01-02") >= stagedChange.EffectiveDate {
|
||||||
|
for _, sh := range stagedChange.Hours {
|
||||||
|
if sh.Weekday == weekday {
|
||||||
|
baseStart = sh.StartTime
|
||||||
|
baseEnd = sh.EndTime
|
||||||
|
isOpen = sh.IsOpen
|
||||||
|
day.Source = "default"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if def, ok := defaultMap[weekday]; ok {
|
} else if def, ok := defaultMap[weekday]; ok {
|
||||||
baseStart = def.StartTime
|
baseStart = def.StartTime
|
||||||
baseEnd = def.EndTime
|
baseEnd = def.EndTime
|
||||||
@@ -901,3 +981,260 @@ func GetPreviewAvailableHours(w http.ResponseWriter, r *http.Request) {
|
|||||||
log.Printf("Failed to encode JSON response: %v", err)
|
log.Printf("Failed to encode JSON response: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Staged Default Hours Change Handlers ---
|
||||||
|
|
||||||
|
// GetDefaultHoursConflictingBookings checks which active bookings would conflict
|
||||||
|
// with proposed default hours if they take effect from a given date.
|
||||||
|
func GetDefaultHoursConflictingBookings(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req struct {
|
||||||
|
ProposedHours []ExceptionalHours `json:"proposedHours" validate:"required,len=7,dive"`
|
||||||
|
EffectiveDate string `json:"effective_date" validate:"required"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
mw.RespondError(w, http.StatusBadRequest, "invalid request body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(req.ProposedHours) != 7 {
|
||||||
|
http.Error(w, "must provide exactly 7 weekday entries (0-6)", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
effDate, err := time.Parse("2006-01-02", req.EffectiveDate)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "invalid effective_date format, expected YYYY-MM-DD", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build proposedByWeekday map
|
||||||
|
proposedByWeekday := map[int]ExceptionalHours{}
|
||||||
|
for _, h := range req.ProposedHours {
|
||||||
|
proposedByWeekday[h.Weekday] = h
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query range: effective date to 90 days out
|
||||||
|
rangeStart := time.Date(effDate.Year(), effDate.Month(), effDate.Day(), 0, 0, 0, 0, londonLocation)
|
||||||
|
rangeEnd := rangeStart.AddDate(0, 0, 90)
|
||||||
|
|
||||||
|
rows, err := db.Conn.Query(r.Context(), `
|
||||||
|
SELECT
|
||||||
|
b.id, b.start_time, b.status, b.created_at,
|
||||||
|
b.total_duration_minutes as duration,
|
||||||
|
COALESCE(u.id, '') as user_id,
|
||||||
|
COALESCE(u.fn, '') as full_name,
|
||||||
|
u.email, u.phone
|
||||||
|
FROM bookings b
|
||||||
|
LEFT JOIN users u ON b.user_id = u.id
|
||||||
|
WHERE `+ActiveBookingStatuses+`
|
||||||
|
AND b.start_time < $2
|
||||||
|
AND b.end_time > $1
|
||||||
|
ORDER BY b.start_time
|
||||||
|
`, rangeStart, rangeEnd)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to query conflicting bookings: %v", err)
|
||||||
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
allConflicts := []OverlappingBooking{}
|
||||||
|
for rows.Next() {
|
||||||
|
var ob OverlappingBooking
|
||||||
|
ob.User = &UserSummary{}
|
||||||
|
if err := rows.Scan(&ob.ID, &ob.StartTime, &ob.Status, &ob.CreatedAt, &ob.Duration, &ob.User.ID, &ob.User.FullName, &ob.User.Email, &ob.User.Phone); err != nil {
|
||||||
|
log.Printf("Failed to scan conflicting booking: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
bookingLondon := ob.StartTime.In(londonLocation)
|
||||||
|
ourWeekday := int((bookingLondon.Weekday() + 6) % 7)
|
||||||
|
|
||||||
|
proposed, _ := proposedByWeekday[ourWeekday]
|
||||||
|
isConflict := false
|
||||||
|
if !proposed.IsOpen {
|
||||||
|
isConflict = true
|
||||||
|
} else {
|
||||||
|
startMinutes := bookingLondon.Hour()*60 + bookingLondon.Minute()
|
||||||
|
endLondon := ob.StartTime.Add(time.Duration(ob.Duration) * time.Minute).In(londonLocation)
|
||||||
|
endMinutes := endLondon.Hour()*60 + endLondon.Minute()
|
||||||
|
propStart := parseTimeToMinutes(proposed.StartTime)
|
||||||
|
propEnd := parseTimeToMinutes(proposed.EndTime)
|
||||||
|
if endMinutes < startMinutes {
|
||||||
|
isConflict = true
|
||||||
|
} else if startMinutes < propStart || endMinutes > propEnd {
|
||||||
|
isConflict = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isConflict {
|
||||||
|
allConflicts = append(allConflicts, ob)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
log.Printf("Error iterating conflicting bookings: %v", err)
|
||||||
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch-load services
|
||||||
|
if len(allConflicts) > 0 {
|
||||||
|
ids := make([]string, len(allConflicts))
|
||||||
|
for i, ob := range allConflicts {
|
||||||
|
ids[i] = ob.ID
|
||||||
|
}
|
||||||
|
serviceRows, err := db.Conn.Query(r.Context(), `
|
||||||
|
SELECT booking_id, name FROM (
|
||||||
|
SELECT bs.booking_id, s.name
|
||||||
|
FROM booking_services bs JOIN services s ON bs.service_id = s.id
|
||||||
|
WHERE bs.booking_id = ANY($1)
|
||||||
|
UNION ALL
|
||||||
|
SELECT bcs.booking_id, cs.name
|
||||||
|
FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id
|
||||||
|
WHERE bcs.booking_id = ANY($1)
|
||||||
|
) sub ORDER BY booking_id, name
|
||||||
|
`, ids)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to batch-load services: %v", err)
|
||||||
|
} else {
|
||||||
|
serviceMap := map[string][]string{}
|
||||||
|
for serviceRows.Next() {
|
||||||
|
var bookingID, name string
|
||||||
|
if serviceRows.Scan(&bookingID, &name) == nil {
|
||||||
|
serviceMap[bookingID] = append(serviceMap[bookingID], name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
serviceRows.Close()
|
||||||
|
for i, ob := range allConflicts {
|
||||||
|
allConflicts[i].Services = serviceMap[ob.ID]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if allConflicts == nil {
|
||||||
|
allConflicts = []OverlappingBooking{}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(OverlappingBookingsResponse{Bookings: allConflicts})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScheduleDefaultHoursChange creates a staged default hours change.
|
||||||
|
func ScheduleDefaultHoursChange(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req struct {
|
||||||
|
Hours []DefaultHours `json:"hours" validate:"required,len=7,dive"`
|
||||||
|
EffectiveDate string `json:"effective_date" validate:"required"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
mw.RespondError(w, http.StatusBadRequest, "invalid request body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate effective_date is in the future
|
||||||
|
effDate, err := time.Parse("2006-01-02", req.EffectiveDate)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "invalid effective_date format, expected YYYY-MM-DD", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Convert to London timezone so comparison with today is DST-safe
|
||||||
|
// (time.Parse yields UTC midnight, but London midnight may differ by 1h during BST).
|
||||||
|
effDate = time.Date(effDate.Year(), effDate.Month(), effDate.Day(), 0, 0, 0, 0, londonLocation)
|
||||||
|
londonNow := clock.Now().In(londonLocation)
|
||||||
|
today := time.Date(londonNow.Year(), londonNow.Month(), londonNow.Day(), 0, 0, 0, 0, londonLocation)
|
||||||
|
if !effDate.After(today) {
|
||||||
|
http.Error(w, "effective_date must be tomorrow or later", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check there isn't already a pending change
|
||||||
|
var existingID int
|
||||||
|
err = db.Conn.QueryRow(r.Context(), `
|
||||||
|
SELECT id FROM default_hours_scheduled_changes
|
||||||
|
WHERE applied_at IS NULL AND cancelled_at IS NULL
|
||||||
|
LIMIT 1
|
||||||
|
`).Scan(&existingID)
|
||||||
|
if err == nil {
|
||||||
|
http.Error(w, "A pending default hours change already exists. Cancel it first.", http.StatusConflict)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each hour entry must pass validation
|
||||||
|
for _, h := range req.Hours {
|
||||||
|
if err := validators.Validate.Struct(&h); err != nil {
|
||||||
|
mw.RespondError(w, http.StatusBadRequest, "Invalid hours data")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get admin user ID from context
|
||||||
|
adminID, _ := r.Context().Value(mw.UserIDKey).(string)
|
||||||
|
|
||||||
|
hoursBytes, err := json.Marshal(req.Hours)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "failed to serialize hours", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.Conn.Exec(r.Context(), `
|
||||||
|
INSERT INTO default_hours_scheduled_changes (effective_date, created_by, hours)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
`, effDate, adminID, string(hoursBytes))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to insert scheduled change: %v", err)
|
||||||
|
http.Error(w, "Failed to schedule change", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{
|
||||||
|
"effective_date": req.EffectiveDate,
|
||||||
|
"message": "Default hours will change at 23:59 on " + effDate.In(londonLocation).Format("02/01/2006"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetScheduledDefaultHoursChange returns the pending change, if any.
|
||||||
|
func GetScheduledDefaultHoursChange(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var effDate, createdAt, createdBy *string
|
||||||
|
var hoursJSON *string
|
||||||
|
err := db.Conn.QueryRow(r.Context(), `
|
||||||
|
SELECT effective_date::text, created_at::text, created_by, hours::text
|
||||||
|
FROM default_hours_scheduled_changes
|
||||||
|
WHERE applied_at IS NULL AND cancelled_at IS NULL
|
||||||
|
LIMIT 1
|
||||||
|
`).Scan(&effDate, &createdAt, &createdBy, &hoursJSON)
|
||||||
|
if err != nil || effDate == nil || hoursJSON == nil {
|
||||||
|
http.Error(w, "no pending change", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var scheduledHours []DefaultHours
|
||||||
|
json.Unmarshal([]byte(*hoursJSON), &scheduledHours)
|
||||||
|
|
||||||
|
resp := ScheduledHoursChange{
|
||||||
|
EffectiveDate: *effDate,
|
||||||
|
Hours: scheduledHours,
|
||||||
|
CreatedAt: *createdAt,
|
||||||
|
CreatedBy: *createdBy,
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CancelScheduledDefaultHoursChange cancels a pending change.
|
||||||
|
func CancelScheduledDefaultHoursChange(w http.ResponseWriter, r *http.Request) {
|
||||||
|
result, err := db.Conn.Exec(r.Context(), `
|
||||||
|
UPDATE default_hours_scheduled_changes
|
||||||
|
SET cancelled_at = NOW()
|
||||||
|
WHERE applied_at IS NULL AND cancelled_at IS NULL
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to cancel scheduled change: %v", err)
|
||||||
|
http.Error(w, "Failed to cancel", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result.RowsAffected() == 0 {
|
||||||
|
http.Error(w, "no pending change to cancel", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|||||||
@@ -323,6 +323,10 @@ func main() {
|
|||||||
r.Get("/preview-available-hours", scheduling.GetPreviewAvailableHours)
|
r.Get("/preview-available-hours", scheduling.GetPreviewAvailableHours)
|
||||||
|
|
||||||
r.Put("/default-hours", scheduling.UpdateDefaultHours)
|
r.Put("/default-hours", scheduling.UpdateDefaultHours)
|
||||||
|
r.Post("/default-hours/conflicting", scheduling.GetDefaultHoursConflictingBookings)
|
||||||
|
r.Post("/default-hours/schedule", scheduling.ScheduleDefaultHoursChange)
|
||||||
|
r.Get("/default-hours/scheduled", scheduling.GetScheduledDefaultHoursChange)
|
||||||
|
r.Delete("/default-hours/scheduled", scheduling.CancelScheduledDefaultHoursChange)
|
||||||
r.Post("/exceptional-groups", scheduling.CreateExceptionalGroup)
|
r.Post("/exceptional-groups", scheduling.CreateExceptionalGroup)
|
||||||
r.Delete("/exceptional-groups", scheduling.DeleteExceptionalGroup)
|
r.Delete("/exceptional-groups", scheduling.DeleteExceptionalGroup)
|
||||||
r.Put("/exceptional-applications", scheduling.UpdateExceptionalApplications)
|
r.Put("/exceptional-applications", scheduling.UpdateExceptionalApplications)
|
||||||
|
|||||||
Reference in New Issue
Block a user