fix calendar again

This commit is contained in:
2025-10-20 20:19:31 +01:00
parent 17ebaeba94
commit ded1081a16
2 changed files with 99 additions and 113 deletions
+85 -105
View File
@@ -86,7 +86,7 @@ func UpdateDefaultHours(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent) w.WriteHeader(http.StatusNoContent)
} }
// --- GetWorkingHours (merged default + applied exceptions) --- // --- GetWorkingHours (merged default + applied exceptions, UK-local) ---
func GetWorkingHours(w http.ResponseWriter, r *http.Request) { func GetWorkingHours(w http.ResponseWriter, r *http.Request) {
startStr := r.URL.Query().Get("start") startStr := r.URL.Query().Get("start")
endStr := r.URL.Query().Get("end") endStr := r.URL.Query().Get("end")
@@ -94,17 +94,23 @@ func GetWorkingHours(w http.ResponseWriter, r *http.Request) {
http.Error(w, "start and end query params required", http.StatusBadRequest) http.Error(w, "start and end query params required", http.StatusBadRequest)
return return
} }
start, err := time.Parse("2006-01-02", startStr)
ukLocation, _ := time.LoadLocation("Europe/London")
start, err := time.ParseInLocation("2006-01-02", startStr, ukLocation)
if err != nil { if err != nil {
http.Error(w, "invalid start date", http.StatusBadRequest) http.Error(w, "invalid start date", http.StatusBadRequest)
return return
} }
end, err := time.Parse("2006-01-02", endStr) end, err := time.ParseInLocation("2006-01-02", endStr, ukLocation)
if err != nil { if err != nil {
http.Error(w, "invalid end date", http.StatusBadRequest) http.Error(w, "invalid end date", http.StatusBadRequest)
return return
} }
// Set to local start/end of day
start = time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, ukLocation)
end = time.Date(end.Year(), end.Month(), end.Day(), 23, 59, 59, 999999999, ukLocation)
// Load default hours // Load default hours
defaultMap := map[int]DefaultHours{} defaultMap := map[int]DefaultHours{}
defRows, _ := db.DB.Query(r.Context(), ` defRows, _ := db.DB.Query(r.Context(), `
@@ -119,35 +125,31 @@ func GetWorkingHours(w http.ResponseWriter, r *http.Request) {
} }
defRows.Close() defRows.Close()
// Load exceptional applications in range // Load exceptional applications for Mondays in range
appRows, _ := db.DB.Query(r.Context(), ` appRows, _ := db.DB.Query(r.Context(), `
SELECT a.group_id, a.week_start SELECT group_id, week_start
FROM exceptional_group_applications a FROM exceptional_group_applications
WHERE a.week_start <= $1 AND a.week_start >= $2 - INTERVAL '6 days' WHERE week_start BETWEEN $1 AND $2
`, end, start) // any week overlapping the range `, start, end)
type appEntry struct { type appEntry struct {
GroupID int GroupID int
WeekStart time.Time WeekStart time.Time
} }
var apps []appEntry apps := []appEntry{}
groupIDs := []int{}
for appRows.Next() { for appRows.Next() {
var e appEntry var a appEntry
if err := appRows.Scan(&e.GroupID, &e.WeekStart); err == nil { if err := appRows.Scan(&a.GroupID, &a.WeekStart); err == nil {
apps = append(apps, e) a.WeekStart = time.Date(a.WeekStart.Year(), a.WeekStart.Month(), a.WeekStart.Day(), 0, 0, 0, 0, ukLocation)
apps = append(apps, a)
groupIDs = append(groupIDs, a.GroupID)
} }
} }
appRows.Close() appRows.Close()
// Load exceptional exHoursMap := map[int]map[int]ExceptionalHours{}
// Load exceptional hours for all relevant groups
groupIDs := []int{}
for _, a := range apps {
groupIDs = append(groupIDs, a.GroupID)
}
exHoursMap := map[int]map[int]ExceptionalHours{} // groupID -> weekday -> hours
if len(groupIDs) > 0 { if len(groupIDs) > 0 {
query, args, _ := sqlIn("SELECT group_id, weekday, start_time::text, end_time::text, is_open FROM exceptional_working_hours WHERE group_id IN (?)", groupIDs) query, args, _ := sqlIn("SELECT group_id, weekday, start_time::text, end_time::text, is_open FROM exceptional_working_hours WHERE group_id IN (%s)", groupIDs)
rows, _ := db.DB.Query(r.Context(), query, args...) rows, _ := db.DB.Query(r.Context(), query, args...)
for rows.Next() { for rows.Next() {
var h ExceptionalHours var h ExceptionalHours
@@ -168,14 +170,22 @@ func GetWorkingHours(w http.ResponseWriter, r *http.Request) {
if weekday == 0 { if weekday == 0 {
weekday = 6 // Go Sunday=0 -> our Sunday=6 weekday = 6 // Go Sunday=0 -> our Sunday=6
} else { } else {
weekday -= 1 // shift Monday=0 ... Sunday=6 weekday -= 1
} }
// find applied group for this week // Calculate the Monday of this week
daysSinceMonday := int(d.Weekday()) - 1
if daysSinceMonday < 0 {
daysSinceMonday = 6 // Sunday
}
weekStart := d.AddDate(0, 0, -daysSinceMonday)
weekStart = time.Date(weekStart.Year(), weekStart.Month(), weekStart.Day(), 0, 0, 0, 0, ukLocation)
var applied *ExceptionalHours var applied *ExceptionalHours
weekStart := d.AddDate(0, 0, -weekday) // Monday of current week weekStartStr := weekStart.Format("2006-01-02")
for _, a := range apps { for _, a := range apps {
if a.WeekStart.Equal(weekStart) { appWeekStartStr := a.WeekStart.Format("2006-01-02")
if appWeekStartStr == weekStartStr {
if dayHours, ok := exHoursMap[a.GroupID][weekday]; ok { if dayHours, ok := exHoursMap[a.GroupID][weekday]; ok {
applied = &dayHours applied = &dayHours
} }
@@ -240,7 +250,7 @@ type DayAvailableHours struct {
Source string `json:"source"` Source string `json:"source"`
} }
// --- GetAvailableHours (working hours with gaps for lunch, bookings, etc.) --- // --- GetAvailableHours (with bookings, UK-local) ---
func GetAvailableHours(w http.ResponseWriter, r *http.Request) { func GetAvailableHours(w http.ResponseWriter, r *http.Request) {
startStr := r.URL.Query().Get("start") startStr := r.URL.Query().Get("start")
endStr := r.URL.Query().Get("end") endStr := r.URL.Query().Get("end")
@@ -249,35 +259,17 @@ func GetAvailableHours(w http.ResponseWriter, r *http.Request) {
return return
} }
// Load UK timezone ukLocation, _ := time.LoadLocation("Europe/London")
ukLocation, err := time.LoadLocation("Europe/London") start, _ := time.ParseInLocation("2006-01-02", startStr, ukLocation)
if err != nil { end, _ := time.ParseInLocation("2006-01-02", endStr, ukLocation)
http.Error(w, "failed to load timezone", http.StatusInternalServerError)
return
}
// Parse dates in UK timezone // set start/end of day
start, err := time.ParseInLocation("2006-01-02", startStr, ukLocation)
if err != nil {
http.Error(w, "invalid start date", http.StatusBadRequest)
return
}
end, err := time.ParseInLocation("2006-01-02", endStr, ukLocation)
if err != nil {
http.Error(w, "invalid end date", http.StatusBadRequest)
return
}
// Set to start of day (00:00:00) and end of day (23:59:59)
start = time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, ukLocation) start = time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, ukLocation)
end = time.Date(end.Year(), end.Month(), end.Day(), 23, 59, 59, 999999999, ukLocation) end = time.Date(end.Year(), end.Month(), end.Day(), 23, 59, 59, 999999999, ukLocation)
// Load default hours // Load default hours
defaultMap := map[int]DefaultHours{} defaultMap := map[int]DefaultHours{}
defRows, _ := db.DB.Query(r.Context(), ` defRows, _ := db.DB.Query(r.Context(), `SELECT weekday, start_time::text, end_time::text, is_open FROM working_hours`)
SELECT weekday, start_time::text, end_time::text, is_open
FROM working_hours
`)
for defRows.Next() { for defRows.Next() {
var d DefaultHours var d DefaultHours
if err := defRows.Scan(&d.Weekday, &d.StartTime, &d.EndTime, &d.IsOpen); err == nil { if err := defRows.Scan(&d.Weekday, &d.StartTime, &d.EndTime, &d.IsOpen); err == nil {
@@ -286,34 +278,31 @@ func GetAvailableHours(w http.ResponseWriter, r *http.Request) {
} }
defRows.Close() defRows.Close()
// Load exceptional applications in range // Load exceptional applications for Mondays in range
appRows, _ := db.DB.Query(r.Context(), ` appRows, _ := db.DB.Query(r.Context(), `
SELECT a.group_id, a.week_start SELECT group_id, week_start
FROM exceptional_group_applications a FROM exceptional_group_applications
WHERE a.week_start <= $1 AND a.week_start >= $2 - INTERVAL '6 days' WHERE week_start BETWEEN $1 AND $2
`, end, start) `, start, end)
type appEntry struct { type appEntry struct {
GroupID int GroupID int
WeekStart time.Time WeekStart time.Time
} }
var apps []appEntry apps := []appEntry{}
groupIDs := []int{}
for appRows.Next() { for appRows.Next() {
var e appEntry var a appEntry
if err := appRows.Scan(&e.GroupID, &e.WeekStart); err == nil { if err := appRows.Scan(&a.GroupID, &a.WeekStart); err == nil {
apps = append(apps, e) a.WeekStart = time.Date(a.WeekStart.Year(), a.WeekStart.Month(), a.WeekStart.Day(), 0, 0, 0, 0, ukLocation)
apps = append(apps, a)
groupIDs = append(groupIDs, a.GroupID)
} }
} }
appRows.Close() appRows.Close()
// Load exceptional hours for all relevant groups exHoursMap := map[int]map[int]ExceptionalHours{}
groupIDs := []int{}
for _, a := range apps {
groupIDs = append(groupIDs, a.GroupID)
}
exHoursMap := map[int]map[int]ExceptionalHours{} // groupID -> weekday -> hours
if len(groupIDs) > 0 { if len(groupIDs) > 0 {
query, args, _ := sqlIn("SELECT group_id, weekday, start_time::text, end_time::text, is_open FROM exceptional_working_hours WHERE group_id IN (?)", groupIDs) query, args, _ := sqlIn("SELECT group_id, weekday, start_time::text, end_time::text, is_open FROM exceptional_working_hours WHERE group_id IN (%s)", groupIDs)
rows, _ := db.DB.Query(r.Context(), query, args...) rows, _ := db.DB.Query(r.Context(), query, args...)
for rows.Next() { for rows.Next() {
var h ExceptionalHours var h ExceptionalHours
@@ -327,58 +316,56 @@ func GetAvailableHours(w http.ResponseWriter, r *http.Request) {
rows.Close() rows.Close()
} }
// Load bookings with their total duration (sum of all services) // Load bookings
bookingRows, err := db.DB.Query(r.Context(), ` bookingRows, _ := db.DB.Query(r.Context(), `
SELECT SELECT b.start_time, COALESCE(SUM(s.duration_minutes),0) as total_duration
b.start_time,
COALESCE(SUM(s.duration_minutes), 0) as total_duration
FROM bookings b FROM bookings b
LEFT JOIN booking_services bs ON b.id = bs.booking_id LEFT JOIN booking_services bs ON b.id = bs.booking_id
LEFT JOIN services s ON bs.service_id = s.id LEFT JOIN services s ON bs.service_id = s.id
WHERE b.start_time >= $1 WHERE b.start_time >= $1 AND b.start_time <= $2
AND b.start_time <= $2
GROUP BY b.id, b.start_time GROUP BY b.id, b.start_time
ORDER BY b.start_time ORDER BY b.start_time
`, start, end) `, start, end)
if err != nil { bookings := map[string][]TimeSlot{}
http.Error(w, "failed to query bookings", http.StatusInternalServerError)
return
}
bookings := map[string][]TimeSlot{} // date -> booked slots
for bookingRows.Next() { for bookingRows.Next() {
var startTime time.Time var t time.Time
var durationMinutes int var dur int
if err := bookingRows.Scan(&startTime, &durationMinutes); err == nil { if err := bookingRows.Scan(&t, &dur); err == nil {
// Convert to UK timezone for date formatting t = t.In(ukLocation)
startTimeUK := startTime.In(ukLocation) dateStr := t.Format("2006-01-02")
dateStr := startTimeUK.Format("2006-01-02") endTime := t.Add(time.Duration(dur) * time.Minute)
endTime := startTimeUK.Add(time.Duration(durationMinutes) * time.Minute)
bookings[dateStr] = append(bookings[dateStr], TimeSlot{ bookings[dateStr] = append(bookings[dateStr], TimeSlot{
StartTime: startTimeUK.Format("15:04"), StartTime: t.Format("15:04"),
EndTime: endTime.Format("15:04"), EndTime: endTime.Format("15:04"),
}) })
} }
} }
bookingRows.Close() bookingRows.Close()
// Generate final result per day with gaps // Generate available slots per day
var results []DayAvailableHours var results []DayAvailableHours
for d := start; !d.After(end); d = d.AddDate(0, 0, 1) { for d := start; !d.After(end); d = d.AddDate(0, 0, 1) {
weekday := int(d.Weekday()) weekday := int(d.Weekday())
if weekday == 0 { if weekday == 0 {
weekday = 6 // Go Sunday=0 -> our Sunday=6 weekday = 6
} else { } else {
weekday -= 1 // shift Monday=0 ... Sunday=6 weekday -= 1
} }
// Find applied group for this week // Calculate the Monday of this week
daysSinceMonday := int(d.Weekday()) - 1
if daysSinceMonday < 0 {
daysSinceMonday = 6 // Sunday
}
weekStart := d.AddDate(0, 0, -daysSinceMonday)
weekStart = time.Date(weekStart.Year(), weekStart.Month(), weekStart.Day(), 0, 0, 0, 0, ukLocation)
var applied *ExceptionalHours var applied *ExceptionalHours
weekStart := d.AddDate(0, 0, -weekday) // Monday of current week weekStartStr := weekStart.Format("2006-01-02")
for _, a := range apps { for _, a := range apps {
if a.WeekStart.Equal(weekStart) { appWeekStartStr := a.WeekStart.Format("2006-01-02")
if appWeekStartStr == weekStartStr {
if dayHours, ok := exHoursMap[a.GroupID][weekday]; ok { if dayHours, ok := exHoursMap[a.GroupID][weekday]; ok {
applied = &dayHours applied = &dayHours
} }
@@ -390,7 +377,6 @@ func GetAvailableHours(w http.ResponseWriter, r *http.Request) {
day.Date = d.Format("2006-01-02") day.Date = d.Format("2006-01-02")
day.Weekday = weekday day.Weekday = weekday
// Determine base working hours and source
var baseStart, baseEnd string var baseStart, baseEnd string
var isOpen bool var isOpen bool
if applied != nil { if applied != nil {
@@ -411,18 +397,12 @@ func GetAvailableHours(w http.ResponseWriter, r *http.Request) {
} }
day.IsOpen = isOpen day.IsOpen = isOpen
if isOpen { if isOpen {
// Start with the full working hours as one slot slots := []TimeSlot{{StartTime: baseStart, EndTime: baseEnd}}
allSlots := []TimeSlot{{StartTime: baseStart, EndTime: baseEnd}} if booked, ok := bookings[day.Date]; ok {
slots = subtractTimeSlots(slots, booked)
// Apply gaps from bookings
dateStr := day.Date
if booked, ok := bookings[dateStr]; ok {
allSlots = subtractTimeSlots(allSlots, booked)
} }
day.Slots = slots
day.Slots = allSlots
} else { } else {
day.Slots = []TimeSlot{} day.Slots = []TimeSlot{}
} }
@@ -142,8 +142,9 @@ func CreateExceptionalGroup(w http.ResponseWriter, r *http.Request) {
} }
var parsedWeeks []time.Time var parsedWeeks []time.Time
ukLocation, _ := time.LoadLocation("Europe/London")
for _, ws := range g.WeekStarts { for _, ws := range g.WeekStarts {
weekStart, err := time.Parse("2006-01-02", ws) weekStart, err := time.ParseInLocation("2006-01-02", ws, ukLocation)
if err != nil { if err != nil {
http.Error(w, "invalid week_start format, expected YYYY-MM-DD", http.StatusBadRequest) http.Error(w, "invalid week_start format, expected YYYY-MM-DD", http.StatusBadRequest)
return return
@@ -152,6 +153,8 @@ func CreateExceptionalGroup(w http.ResponseWriter, r *http.Request) {
http.Error(w, "week_start must be a Monday", http.StatusBadRequest) http.Error(w, "week_start must be a Monday", http.StatusBadRequest)
return return
} }
// Normalize to UK midnight
weekStart = time.Date(weekStart.Year(), weekStart.Month(), weekStart.Day(), 0, 0, 0, 0, ukLocation)
parsedWeeks = append(parsedWeeks, weekStart) parsedWeeks = append(parsedWeeks, weekStart)
} }
@@ -262,8 +265,9 @@ func UpdateExceptionalApplications(w http.ResponseWriter, r *http.Request) {
// Validate and parse weeks // Validate and parse weeks
var parsedWeeks []time.Time var parsedWeeks []time.Time
ukLocation, _ := time.LoadLocation("Europe/London")
for _, ws := range req.WeekStarts { for _, ws := range req.WeekStarts {
weekStart, err := time.Parse("2006-01-02", ws) weekStart, err := time.ParseInLocation("2006-01-02", ws, ukLocation)
if err != nil { if err != nil {
http.Error(w, "invalid week_start format, expected YYYY-MM-DD", http.StatusBadRequest) http.Error(w, "invalid week_start format, expected YYYY-MM-DD", http.StatusBadRequest)
return return
@@ -272,6 +276,8 @@ func UpdateExceptionalApplications(w http.ResponseWriter, r *http.Request) {
http.Error(w, "week_start must be a Monday", http.StatusBadRequest) http.Error(w, "week_start must be a Monday", http.StatusBadRequest)
return return
} }
// Normalize to UK midnight
weekStart = time.Date(weekStart.Year(), weekStart.Month(), weekStart.Day(), 0, 0, 0, 0, ukLocation)
parsedWeeks = append(parsedWeeks, weekStart) parsedWeeks = append(parsedWeeks, weekStart)
} }