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)
|
blockerMap := make(map[string][]TimeSlot)
|
||||||
|
|
||||||
for _, blocker := range blockers {
|
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)
|
dateStr := cur.Format("2006-01-02")
|
||||||
|
endStr := segEnd.Format("15:04")
|
||||||
blockerMap[dateStr] = append(blockerMap[dateStr], TimeSlot{
|
if segEnd.Equal(dayEnd) {
|
||||||
|
endStr = "24:00"
|
||||||
StartTime: blocker.StartTime.Format("15:04"),
|
}
|
||||||
|
blockerMap[dateStr] = append(blockerMap[dateStr], TimeSlot{
|
||||||
EndTime: endTime.Format("15:04"),
|
StartTime: cur.Format("15:04"),
|
||||||
})
|
EndTime: endStr,
|
||||||
|
})
|
||||||
|
|
||||||
|
cur = dayEnd
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user is admin
|
// Check if user is admin
|
||||||
@@ -575,21 +590,28 @@ func GetAvailableHours(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
day.IsOpen = isOpen
|
day.IsOpen = isOpen
|
||||||
if 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}}
|
slots := []TimeSlot{{StartTime: baseStart, EndTime: baseEnd}}
|
||||||
if booked, ok := bookings[day.Date]; ok {
|
if booked, ok := bookings[day.Date]; ok {
|
||||||
slots = subtractTimeSlots(slots, booked)
|
slots = subtractTimeSlots(slots, booked)
|
||||||
}
|
}
|
||||||
day.Slots = slots
|
day.Slots = slots
|
||||||
// Handle time blockers
|
// Subtract time blockers from available slots for ALL users
|
||||||
if !isAdmin {
|
// (prevents showing blocked slots that would fail on reserve)
|
||||||
// Regular users: subtract blockers from available slots
|
if dayBlockers, ok := blockerMap[day.Date]; ok {
|
||||||
if dayBlockers, ok := blockerMap[day.Date]; ok {
|
day.Slots = subtractTimeSlots(day.Slots, dayBlockers)
|
||||||
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()
|
now := time.Now()
|
||||||
if !isAdmin && now.Hour() >= 22 {
|
if now.Hour() >= 22 {
|
||||||
// Check if this is tomorrow's date
|
// Check if this is tomorrow's date
|
||||||
tomorrow := now.AddDate(0, 0, 1)
|
tomorrow := now.AddDate(0, 0, 1)
|
||||||
tomorrowStr := tomorrow.Format("2006-01-02")
|
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})
|
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 {
|
} else {
|
||||||
day.Slots = []TimeSlot{}
|
day.Slots = []TimeSlot{}
|
||||||
@@ -619,6 +636,20 @@ func GetAvailableHours(w http.ResponseWriter, r *http.Request) {
|
|||||||
json.NewEncoder(w).Encode(results)
|
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
|
// subtractTimeSlots removes gaps from available slots
|
||||||
// Returns the remaining available time slots after removing the gaps
|
// Returns the remaining available time slots after removing the gaps
|
||||||
func subtractTimeSlots(available []TimeSlot, gaps []TimeSlot) []TimeSlot {
|
func subtractTimeSlots(available []TimeSlot, gaps []TimeSlot) []TimeSlot {
|
||||||
|
|||||||
Reference in New Issue
Block a user