diff --git a/backend/handlers/scheduling/default-hours.go b/backend/handlers/scheduling/default-hours.go index 8672bb4..39cd84b 100644 --- a/backend/handlers/scheduling/default-hours.go +++ b/backend/handlers/scheduling/default-hours.go @@ -492,18 +492,33 @@ func GetAvailableHours(w http.ResponseWriter, r *http.Request) { blockerMap := make(map[string][]TimeSlot) for _, blocker := range blockers { + blockStart := blocker.StartTime + blockEnd := blockStart.Add(time.Duration(blocker.DurationMinutes) * time.Minute) - dateStr := blocker.StartTime.Format("2006-01-02") + // Split multi-day blockers into per-day segments so subtractTimeSlots + // only compares times within the same calendar day. Each segment's end + // time uses "24:00" for day boundaries (midnight of the next day) since + // "00:00" as an end time would incorrectly appear before all slot times. + cur := blockStart + for cur.Before(blockEnd) { + dayEnd := time.Date(cur.Year(), cur.Month(), cur.Day(), 0, 0, 0, 0, cur.Location()).AddDate(0, 0, 1) + segEnd := blockEnd + if segEnd.After(dayEnd) { + segEnd = dayEnd + } - endTime := blocker.StartTime.Add(time.Duration(blocker.DurationMinutes) * time.Minute) - - blockerMap[dateStr] = append(blockerMap[dateStr], TimeSlot{ - - StartTime: blocker.StartTime.Format("15:04"), - - EndTime: endTime.Format("15:04"), - }) + dateStr := cur.Format("2006-01-02") + endStr := segEnd.Format("15:04") + if segEnd.Equal(dayEnd) { + endStr = "24:00" + } + blockerMap[dateStr] = append(blockerMap[dateStr], TimeSlot{ + StartTime: cur.Format("15:04"), + EndTime: endStr, + }) + cur = dayEnd + } } // Check if user is admin @@ -575,21 +590,28 @@ func GetAvailableHours(w http.ResponseWriter, r *http.Request) { day.IsOpen = isOpen if isOpen { + // Normalize to HH:MM to match blocker and booking time formats + baseStart = normalizeTime(baseStart) + baseEnd = normalizeTime(baseEnd) slots := []TimeSlot{{StartTime: baseStart, EndTime: baseEnd}} if booked, ok := bookings[day.Date]; ok { slots = subtractTimeSlots(slots, booked) } day.Slots = slots - // Handle time blockers - if !isAdmin { - // Regular users: subtract blockers from available slots - if dayBlockers, ok := blockerMap[day.Date]; ok { - day.Slots = subtractTimeSlots(day.Slots, dayBlockers) + // Subtract time blockers from available slots for ALL users + // (prevents showing blocked slots that would fail on reserve) + if dayBlockers, ok := blockerMap[day.Date]; ok { + day.Slots = subtractTimeSlots(day.Slots, dayBlockers) + // Store blockers separately for admin warning display + if isAdmin { + day.Blockers = dayBlockers } + } - // Late night lock: after 22:00, block next morning 00:00-11:00 for non-admin users + // Late night lock: after 22:00, block next morning 00:00-11:00 for non-admin users + if !isAdmin { now := time.Now() - if !isAdmin && now.Hour() >= 22 { + if now.Hour() >= 22 { // Check if this is tomorrow's date tomorrow := now.AddDate(0, 0, 1) tomorrowStr := tomorrow.Format("2006-01-02") @@ -602,11 +624,6 @@ func GetAvailableHours(w http.ResponseWriter, r *http.Request) { day.Slots = subtractTimeSlots(day.Slots, []TimeSlot{lateNightBlock}) } } - } else { - // Admins: keep blockers visible for warning display - if dayBlockers, ok := blockerMap[day.Date]; ok { - day.Blockers = dayBlockers - } } } else { day.Slots = []TimeSlot{} @@ -619,6 +636,20 @@ func GetAvailableHours(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(results) } +// normalizeTime strips seconds from HH:MM:SS to HH:MM for consistent string +// comparison with blocker and booking time formats in subtractTimeSlots. +func normalizeTime(t string) string { + // Strip trailing :SS (seconds) from HH:MM:SS format while leaving + // bare HH:MM untouched and handling single-digit hours (e.g. 9:00:00). + if len(t) > 5 && t[len(t)-3] == ':' { + prefix := t[:len(t)-3] + if strings.Contains(prefix, ":") { + return prefix + } + } + return t +} + // subtractTimeSlots removes gaps from available slots // Returns the remaining available time slots after removing the gaps func subtractTimeSlots(available []TimeSlot, gaps []TimeSlot) []TimeSlot {