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