refactor(scheduling): split multi-day blockers and normalize time strings
Split multi-day time blockers into per-day segments for subtractTimeSlots. Add normalizeTime helper to strip seconds from HH:MM:SS for consistent comparison. Subtract blockers for all users (not just non-admin) to prevent 409 errors on reserve. Move late-night lock logic to separate block. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user