diff --git a/backend/handlers/scheduling/exceptional-hours.go b/backend/handlers/scheduling/exceptional-hours.go index 1b0e5d6..99f67f9 100644 --- a/backend/handlers/scheduling/exceptional-hours.go +++ b/backend/handlers/scheduling/exceptional-hours.go @@ -3,6 +3,7 @@ package scheduling import ( "encoding/json" "net/http" + "strconv" "time" "crussell/db" @@ -21,19 +22,14 @@ type ExceptionalGroup struct { ID int `json:"id"` Name string `json:"name"` Description string `json:"description"` - Hours []ExceptionalHours `json:"hours,omitempty"` // opening hours per day, 7 (day) entries per week + Hours []ExceptionalHours `json:"hours,omitempty"` + WeekStarts []string `json:"weekStarts,omitempty"` } -type ExceptionalApplication struct { - ID int `json:"id"` - GroupID int `json:"groupId"` - WeekStart string `json:"weekStart"` // week beginning monday the Xth -} - -// --- Exceptional Groups & Hours --- +// --- List Groups with Hours and Applications --- func ListExceptionalGroups(w http.ResponseWriter, r *http.Request) { rows, err := db.DB.Query(r.Context(), ` - SELECT id,name,description + SELECT id, name, description FROM exceptional_working_hours_groups ORDER BY id DESC `) @@ -50,125 +46,267 @@ func ListExceptionalGroups(w http.ResponseWriter, r *http.Request) { http.Error(w, "failed to scan group", http.StatusInternalServerError) return } - // load 7-day hours - hoursRows, _ := db.DB.Query(r.Context(), ` + + // Load 7-day hours + hoursRows, err := db.DB.Query(r.Context(), ` SELECT id, weekday, start_time::text, end_time::text, is_open FROM exceptional_working_hours WHERE group_id=$1 ORDER BY weekday `, g.ID) + if err != nil { + http.Error(w, "failed to fetch group hours", http.StatusInternalServerError) + return + } + for hoursRows.Next() { var h ExceptionalHours - if err := hoursRows.Scan(&h.ID, &h.Weekday, &h.StartTime, &h.EndTime, &h.IsOpen); err == nil { - h.GroupID = g.ID - g.Hours = append(g.Hours, h) + if err := hoursRows.Scan(&h.ID, &h.Weekday, &h.StartTime, &h.EndTime, &h.IsOpen); err != nil { + hoursRows.Close() + http.Error(w, "failed to scan hours", http.StatusInternalServerError) + return } + h.GroupID = g.ID + g.Hours = append(g.Hours, h) } hoursRows.Close() + + if err := hoursRows.Err(); err != nil { + http.Error(w, "error iterating hours", http.StatusInternalServerError) + return + } + + // Load applied week starts + weekRows, err := db.DB.Query(r.Context(), ` + SELECT week_start + FROM exceptional_group_applications + WHERE group_id=$1 ORDER BY week_start + `, g.ID) + if err != nil { + http.Error(w, "failed to fetch applications", http.StatusInternalServerError) + return + } + + for weekRows.Next() { + var weekStart time.Time + if err := weekRows.Scan(&weekStart); err != nil { + weekRows.Close() + http.Error(w, "failed to scan week_start", http.StatusInternalServerError) + return + } + g.WeekStarts = append(g.WeekStarts, weekStart.Format("2006-01-02")) + } + weekRows.Close() + + if err := weekRows.Err(); err != nil { + http.Error(w, "error iterating applications", http.StatusInternalServerError) + return + } + groups = append(groups, g) } + if err := rows.Err(); err != nil { + http.Error(w, "error iterating groups", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(groups) } +// --- Create Group with Hours and Applications (bulk) --- func CreateExceptionalGroup(w http.ResponseWriter, r *http.Request) { var g ExceptionalGroup if err := json.NewDecoder(r.Body).Decode(&g); err != nil { http.Error(w, "invalid payload", http.StatusBadRequest) return } + if len(g.Hours) != 7 { - http.Error(w, "must provide 7 weekday entries", http.StatusBadRequest) + http.Error(w, "must provide exactly 7 weekday entries (0-6)", http.StatusBadRequest) return } + // Validate weekdays and parse week_starts + weekdaysSeen := make(map[int]bool) + for _, h := range g.Hours { + if h.Weekday < 0 || h.Weekday > 6 { + http.Error(w, "weekday must be 0-6", http.StatusBadRequest) + return + } + if weekdaysSeen[h.Weekday] { + http.Error(w, "duplicate weekday entries", http.StatusBadRequest) + return + } + weekdaysSeen[h.Weekday] = true + } + + var parsedWeeks []time.Time + for _, ws := range g.WeekStarts { + weekStart, err := time.Parse("2006-01-02", ws) + if err != nil { + http.Error(w, "invalid week_start format, expected YYYY-MM-DD", http.StatusBadRequest) + return + } + if weekStart.Weekday() != time.Monday { + http.Error(w, "week_start must be a Monday", http.StatusBadRequest) + return + } + parsedWeeks = append(parsedWeeks, weekStart) + } + tx, err := db.DB.Begin(r.Context()) if err != nil { - http.Error(w, "failed to start tx", http.StatusInternalServerError) + http.Error(w, "failed to start transaction", http.StatusInternalServerError) return } defer tx.Rollback(r.Context()) + // Create group err = tx.QueryRow(r.Context(), ` INSERT INTO exceptional_working_hours_groups (name, description) - VALUES ($1,$2) RETURNING id + VALUES ($1, $2) RETURNING id `, g.Name, g.Description).Scan(&g.ID) if err != nil { http.Error(w, "failed to create group", http.StatusInternalServerError) return } - for _, h := range g.Hours { - _, err := tx.Exec(r.Context(), ` + // Insert hours and collect their IDs + inputHours := g.Hours + g.Hours = []ExceptionalHours{} // Clear and rebuild with IDs + for _, h := range inputHours { + var id int + err := tx.QueryRow(r.Context(), ` INSERT INTO exceptional_working_hours (group_id, weekday, start_time, end_time, is_open) - VALUES ($1,$2,$3,$4,$5) - `, g.ID, h.Weekday, h.StartTime, h.EndTime, h.IsOpen) + VALUES ($1, $2, $3, $4, $5) + RETURNING id + `, g.ID, h.Weekday, h.StartTime, h.EndTime, h.IsOpen).Scan(&id) if err != nil { - http.Error(w, "failed to insert group hours", http.StatusInternalServerError) + http.Error(w, "failed to insert hours", http.StatusInternalServerError) + return + } + + h.ID = id + h.GroupID = g.ID + g.Hours = append(g.Hours, h) + } + + // Insert applications + for _, weekStart := range parsedWeeks { + _, err := tx.Exec(r.Context(), ` + INSERT INTO exceptional_group_applications (group_id, week_start) + VALUES ($1, $2) + `, g.ID, weekStart) + if err != nil { + http.Error(w, "failed to insert application", http.StatusInternalServerError) return } } if err := tx.Commit(r.Context()); err != nil { - http.Error(w, "failed to commit", http.StatusInternalServerError) + http.Error(w, "failed to commit transaction", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(g) } -// --- Exceptional Applications (assign a group to a week) --- -func ListExceptionalApplications(w http.ResponseWriter, r *http.Request) { - rows, err := db.DB.Query(r.Context(), ` - SELECT id, group_id, week_start - FROM exceptional_group_applications - ORDER BY week_start DESC - `) - if err != nil { - http.Error(w, "failed to fetch applications", http.StatusInternalServerError) +// --- Delete Group (cascades to hours and applications) --- +func DeleteExceptionalGroup(w http.ResponseWriter, r *http.Request) { + // Extract group ID from URL path or query params + // Assuming you have a router that provides this, e.g. chi, mux, etc. + // For this example, we'll use query param: DELETE /exceptional-groups?id=123 + idStr := r.URL.Query().Get("id") + if idStr == "" { + http.Error(w, "missing id parameter", http.StatusBadRequest) return } - defer rows.Close() - var list []ExceptionalApplication - for rows.Next() { - var a ExceptionalApplication - var weekStart time.Time - if err := rows.Scan(&a.ID, &a.GroupID, &weekStart); err != nil { - http.Error(w, "failed to scan application", http.StatusInternalServerError) - return - } - a.WeekStart = weekStart.Format("2006-01-02") - list = append(list, a) + id, err := strconv.Atoi(idStr) + if err != nil { + http.Error(w, "invalid id parameter", http.StatusBadRequest) + return } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(list) + result, err := db.DB.Exec(r.Context(), ` + DELETE FROM exceptional_working_hours_groups WHERE id=$1 + `, id) + if err != nil { + http.Error(w, "failed to delete group", http.StatusInternalServerError) + return + } + + rowsAffected := result.RowsAffected() + if rowsAffected == 0 { + http.Error(w, "group not found", http.StatusNotFound) + return + } + + w.WriteHeader(http.StatusNoContent) } -func CreateExceptionalApplication(w http.ResponseWriter, r *http.Request) { - var a ExceptionalApplication - if err := json.NewDecoder(r.Body).Decode(&a); err != nil { +// --- Update Applied Weeks (replaces all applications for a group) --- +func UpdateExceptionalApplications(w http.ResponseWriter, r *http.Request) { + var req struct { + GroupID int `json:"groupId"` + WeekStarts []string `json:"weekStarts"` + } + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid payload", http.StatusBadRequest) return } - weekStart, err := time.Parse("2006-01-02", a.WeekStart) + + // Validate and parse weeks + var parsedWeeks []time.Time + for _, ws := range req.WeekStarts { + weekStart, err := time.Parse("2006-01-02", ws) + if err != nil { + http.Error(w, "invalid week_start format, expected YYYY-MM-DD", http.StatusBadRequest) + return + } + if weekStart.Weekday() != time.Monday { + http.Error(w, "week_start must be a Monday", http.StatusBadRequest) + return + } + parsedWeeks = append(parsedWeeks, weekStart) + } + + tx, err := db.DB.Begin(r.Context()) if err != nil { - http.Error(w, "invalid week_start format, expected YYYY-MM-DD", http.StatusBadRequest) + http.Error(w, "failed to start transaction", http.StatusInternalServerError) + return + } + defer tx.Rollback(r.Context()) + + // Delete existing applications for this group + _, err = tx.Exec(r.Context(), ` + DELETE FROM exceptional_group_applications WHERE group_id=$1 + `, req.GroupID) + if err != nil { + http.Error(w, "failed to delete existing applications", http.StatusInternalServerError) return } - err = db.DB.QueryRow(r.Context(), ` - INSERT INTO exceptional_group_applications (group_id, week_start) - VALUES ($1,$2) - RETURNING id - `, a.GroupID, weekStart).Scan(&a.ID) - if err != nil { - http.Error(w, "failed to create application", http.StatusInternalServerError) + // Insert new applications + for _, weekStart := range parsedWeeks { + _, err := tx.Exec(r.Context(), ` + INSERT INTO exceptional_group_applications (group_id, week_start) + VALUES ($1, $2) + `, req.GroupID, weekStart) + if err != nil { + http.Error(w, "failed to insert application", http.StatusInternalServerError) + return + } + } + + if err := tx.Commit(r.Context()); err != nil { + http.Error(w, "failed to commit transaction", http.StatusInternalServerError) return } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(a) + w.WriteHeader(http.StatusNoContent) } diff --git a/backend/main.go b/backend/main.go index 4d6615b..caa972d 100644 --- a/backend/main.go +++ b/backend/main.go @@ -77,11 +77,11 @@ func main() { r.Post("/register", authHandlers.RegisterHandler) r.Post("/login", authHandlers.LoginHandler) - // --- Scheduling public GET routes --- + // --- Scheduling Routes --- r.Route("/scheduling", func(r chi.Router) { + // Public GET routes r.Get("/default-hours", scheduling.GetDefaultHours) r.Get("/exceptional-groups", scheduling.ListExceptionalGroups) - r.Get("/exceptional-applications", scheduling.ListExceptionalApplications) r.Get("/working-hours", scheduling.GetWorkingHours) r.Get("/available-hours", scheduling.GetAvailableHours) @@ -92,7 +92,8 @@ func main() { r.Put("/default-hours", scheduling.UpdateDefaultHours) r.Post("/exceptional-groups", scheduling.CreateExceptionalGroup) - r.Post("/exceptional-applications", scheduling.CreateExceptionalApplication) + r.Delete("/exceptional-groups", scheduling.DeleteExceptionalGroup) + r.Put("/exceptional-applications", scheduling.UpdateExceptionalApplications) }) }) diff --git a/frontend/src/routes/admin/+page.svelte b/frontend/src/routes/admin/+page.svelte index 0c6c634..67dd571 100644 --- a/frontend/src/routes/admin/+page.svelte +++ b/frontend/src/routes/admin/+page.svelte @@ -178,43 +178,83 @@ id?: number; name: string; description: string; - week_starts: string[]; - rows: WorkingHourRow[]; + weekStarts: string[]; + hours: WorkingHourRow[]; }; - // DEMO DATA: Exception Groups - let exceptionGroups = $state([ - { - id: 1, - name: 'Christmas Week', - description: 'Closed from Mon-Wed, open reduced hours Thu/Fri', - week_starts: ['2025-12-22'], - rows: [ - { weekday: 0, start_time: '09:00', end_time: '17:00', is_open: false }, - { weekday: 1, start_time: '09:00', end_time: '17:00', is_open: false }, - { weekday: 2, start_time: '09:00', end_time: '17:00', is_open: false }, - { weekday: 3, start_time: '10:00', end_time: '15:00', is_open: true }, - { weekday: 4, start_time: '10:00', end_time: '15:00', is_open: true }, - { weekday: 5, start_time: '09:00', end_time: '17:00', is_open: false }, - { weekday: 6, start_time: '09:00', end_time: '17:00', is_open: false } - ] - }, - { - id: 2, - name: 'Summer Holiday', - description: 'Closed on Mondays/Tuesdays only', - week_starts: ['2026-07-06', '2026-07-13', '2026-07-20'], - rows: [ - { weekday: 0, start_time: '09:00', end_time: '17:00', is_open: false }, - { weekday: 1, start_time: '09:00', end_time: '17:00', is_open: false }, - { weekday: 2, start_time: '09:00', end_time: '17:00', is_open: true }, - { weekday: 3, start_time: '09:00', end_time: '17:00', is_open: true }, - { weekday: 4, start_time: '09:00', end_time: '17:00', is_open: true }, - { weekday: 5, start_time: '09:00', end_time: '17:00', is_open: false }, - { weekday: 6, start_time: '09:00', end_time: '17:00', is_open: false } - ] + // Replace the demo data with empty array and add loading state + let exceptionGroups = $state([]); + let exceptionGroupsLoading = $state(true); + + // Add state for the exception modal + let exceptionDraft = $state({ + name: '', + description: '', + weekStarts: [], + hours: [ + { weekday: 0, start_time: '00:00', end_time: '00:00', is_open: false }, + { weekday: 1, start_time: '09:00', end_time: '17:00', is_open: true }, + { weekday: 2, start_time: '09:00', end_time: '17:00', is_open: true }, + { weekday: 3, start_time: '09:00', end_time: '17:00', is_open: true }, + { weekday: 4, start_time: '12:00', end_time: '20:00', is_open: true }, + { weekday: 5, start_time: '09:00', end_time: '17:00', is_open: true }, + { weekday: 6, start_time: '00:00', end_time: '00:00', is_open: false } + ] + }); + + let weekRangeFrom = $state(''); + let weekRangeTo = $state(''); + + async function fetchExceptionGroups() { + if (pageState !== 'authorized') return; + + exceptionGroupsLoading = true; + try { + const response = await fetch('/api/scheduling/exceptional-groups', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${authStore.currentToken}` + } + }); + + if (response.ok) { + const data = await response.json(); + if (data === null || data.length === 0) { + return; + } + exceptionGroups = data.map((group: any) => ({ + id: group.id, + name: group.name, + description: group.description, + weekStarts: group.weekStarts || [], + hours: + group.hours?.map((h: any) => ({ + id: h.id, + weekday: h.weekday, + start_time: formatTime(h.startTime), + end_time: formatTime(h.endTime), + is_open: h.isOpen + })) || [] + })); + } else { + console.error('Failed to fetch exception groups:', response.status); + toast.error('Failed to load exception groups'); + } + } catch (err) { + console.error('Error fetching exception groups:', err); + toast.error('Network error loading exception groups'); + } finally { + exceptionGroupsLoading = false; } - ]); + } + + // Fetch on mount + $effect(() => { + if (pageState === 'authorized') { + fetchExceptionGroups(); + } + }); let loadingHours = $state(false); let savingHours = $state(false); @@ -283,24 +323,133 @@ } async function saveExceptionGroup() { - // TODO - } + // Validate + if (!exceptionDraft.name.trim()) { + toast.error('Please enter a group name'); + return; + } + + if (exceptionDraft.weekStarts.length === 0) { + toast.error('Please add at least one week'); + return; + } + + savingHours = true; + const loadingToast = toast.loading('Creating exception group...'); - async function confirmDeleteExceptionGroup() { try { - await new Promise((r) => setTimeout(r, 500)); - exceptionGroups = exceptionGroups.filter((g) => g.id !== exceptionToDelete); - showDeleteExceptionAlert = false; - exceptionToDelete = undefined; + // Map to API format + const payload = { + name: exceptionDraft.name, + description: exceptionDraft.description, + weekStarts: exceptionDraft.weekStarts, + hours: exceptionDraft.hours.map((h) => ({ + weekday: h.weekday, + startTime: h.start_time, + endTime: h.end_time, + isOpen: h.is_open + })) + }; + + const response = await fetch('/api/scheduling/exceptional-groups', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${authStore.currentToken}` + }, + body: JSON.stringify(payload) + }); + + if (response.ok) { + toast.success('Exception group created successfully!', { id: loadingToast }); + showExceptionModal = false; + resetExceptionForm(); + await fetchExceptionGroups(); + } else { + const text = await response.text(); + toast.error('Failed to create: ' + text, { id: loadingToast }); + } } catch (err) { - console.error(err); + console.error('Error creating exception group:', err); + toast.error('Network error creating exception group', { id: loadingToast }); + } finally { + savingHours = false; } } + async function confirmDeleteExceptionGroup() { + if (exceptionToDelete === undefined) return; + + const loadingToast = toast.loading('Deleting exception group...'); + + try { + const response = await fetch(`/api/scheduling/exceptional-groups?id=${exceptionToDelete}`, { + method: 'DELETE', + headers: { + Authorization: `Bearer ${authStore.currentToken}` + } + }); + + if (response.ok || response.status === 204) { + toast.success('Exception group deleted successfully!', { id: loadingToast }); + showDeleteExceptionAlert = false; + exceptionToDelete = undefined; + // Refresh the exception groups list + await fetchExceptionGroups(); + } else { + const text = await response.text(); + toast.error('Failed to delete: ' + text, { id: loadingToast }); + } + } catch (err) { + console.error('Error deleting exception group:', err); + toast.error('Network error deleting exception group', { id: loadingToast }); + } + } + + function openViewExceptionModal(exception: ExceptionGroup) { + viewingException = exception; + showViewExceptionModal = true; + } + + function resetExceptionForm() { + exceptionDraft = { + name: '', + description: '', + weekStarts: [], + hours: [ + { weekday: 0, start_time: '00:00', end_time: '00:00', is_open: false }, + { weekday: 1, start_time: '09:00', end_time: '17:00', is_open: true }, + { weekday: 2, start_time: '09:00', end_time: '17:00', is_open: true }, + { weekday: 3, start_time: '09:00', end_time: '17:00', is_open: true }, + { weekday: 4, start_time: '12:00', end_time: '20:00', is_open: true }, + { weekday: 5, start_time: '09:00', end_time: '17:00', is_open: true }, + { weekday: 6, start_time: '00:00', end_time: '00:00', is_open: false } + ] + }; + weekRangeFrom = ''; + weekRangeTo = ''; + } + function createNewException() { + resetExceptionForm(); showExceptionModal = true; } + function addWeekRange() { + if (!weekRangeFrom || !weekRangeTo) { + toast.error('Please select both start and end dates'); + return; + } + + addWeeksToException(weekRangeFrom, weekRangeTo, exceptionDraft.weekStarts); + weekRangeFrom = ''; + weekRangeTo = ''; + } + + function removeWeek(index: number) { + exceptionDraft.weekStarts = exceptionDraft.weekStarts.filter((_, i) => i !== index); + } + // =============== Users & Bookings =============== type User = { id: string; @@ -464,8 +613,12 @@ const to = new Date(toISO + 'T00:00:00'); const first = new Date(from); const day = first.getDay(); - const mondayOffset = (day + 6) % 7; - first.setDate(first.getDate() - mondayOffset); + const daysToMonday = day === 0 ? -6 : 1 - day; + + // Set to the Monday of the current week + first.setDate(first.getDate() + daysToMonday); + + // Add all Mondays in the range for (let d = new Date(first); d <= to; d.setDate(d.getDate() + 7)) { dest.push(isoDateOf(new Date(d))); } @@ -819,6 +972,8 @@ let showSaveDefaultHoursAlert = $state(false); let showDeleteExceptionAlert = $state(false); let exceptionToDelete = $state(undefined); + let showViewExceptionModal = $state(false); + let viewingException = $state(null); {#if pageState === 'loading'} @@ -1130,52 +1285,52 @@ -
- {#if exceptionGroups.length === 0} -

No exception groups found.

- {/if} + {#if exceptionGroupsLoading} +
+ {#each Array(2) as _, i} + + {/each} +
+ {:else} +
+ {#if exceptionGroups.length === 0} +

No exception groups found.

+ {/if} - {#each exceptionGroups as g} -
- -
-
-
{g.name}
-
{g.description}
-
- Applies to weeks: {g.week_starts?.slice(0, 5).join(', ')} - {#if (g.week_starts?.length ?? 0) > 5} - (+{(g.week_starts?.length ?? 0) - 5} more) - {/if} + {#each exceptionGroups as g} +
+
+
+
{g.name}
+
{g.description}
+
+ Applies to weeks: {g.weekStarts?.slice(0, 5).join(', ')} + {#if (g.weekStarts?.length ?? 0) > 5} + (+{(g.weekStarts?.length ?? 0) - 5} more) + {/if} +
+
+ +
+ +
- - -
- - -
-
- {/each} -
+ {/each} +
+ {/if} @@ -1530,21 +1685,149 @@ - - - New Exception Group + + + Create Exception Schedule + + Define custom working hours for holidays, closures, or special events. + - + +
+ +
+
+ + +
+ +
+ + +
+
+ + + + +
+
+

Apply to Weeks *

+

+ Select a date range to add all Mondays within that range +

+ +
+
+ + +
+
+ + +
+
+ + +
+ + {#if exceptionDraft.weekStarts.length > 0} +
+
+ Selected weeks ({exceptionDraft.weekStarts.length}): +
+
+ {#each exceptionDraft.weekStarts as week, index} +
+ Week starting: {week} + +
+ {/each} +
+
+ {/if} +
+ + + + +
+

Working Hours for these Weeks *

+
+ + + + + + + + + + + {#each exceptionDraft.hours as row} + + + + + + + {/each} + +
DayOpenStartEnd
{weekdayLabel(row.weekday)} + + + + + +
+
+
+
+ +
@@ -1573,6 +1856,89 @@ + + {#if viewingException} + + + + {viewingException.name} + + {viewingException.description || 'Holiday schedule details'} + + + +
+ +
+

Applied to Weeks

+
+ {#if viewingException.weekStarts && viewingException.weekStarts.length > 0} +
+ {#each viewingException.weekStarts as week} +
+ Week of {new Date(week).toLocaleDateString('en-GB', { + day: 'numeric', + month: 'short', + year: 'numeric' + })} +
+ {/each} +
+ {:else} +

No weeks specified

+ {/if} +
+
+ + + + +
+

Working Hours

+
+ + + + + + + + + + + {#each viewingException.hours as row} + + + + + + + {/each} + +
DayStatusStartEnd
{weekdayLabel(row.weekday)} + + {row.is_open ? 'Open' : 'Closed'} + + + {row.is_open ? row.start_time : '—'} + + {row.is_open ? row.end_time : '—'} +
+
+
+
+ + + + +
+
+ {/if} + {#if selectedUser} @@ -1669,7 +2035,7 @@ diff --git a/frontend/src/routes/book/+page.svelte b/frontend/src/routes/book/+page.svelte index 39fe4e5..1f42ea8 100644 --- a/frontend/src/routes/book/+page.svelte +++ b/frontend/src/routes/book/+page.svelte @@ -302,7 +302,7 @@ } // Check if we have working hours data - if (!workingHours) return false; + if (!workingHours) return true; // Changed from false to true when data not loaded const dateStr = date.toString(); const dayHours = workingHours[dateStr]; @@ -472,7 +472,13 @@ const dayWorkingHours = workingHours[dateStr]; const dayAvailableHours = availableHours[dateStr]; - if (!dayWorkingHours || !dayWorkingHours.isOpen || !dayAvailableHours) { + // Add check for slots existence + if ( + !dayWorkingHours || + !dayWorkingHours.isOpen || + !dayAvailableHours || + !dayAvailableHours.slots + ) { return []; }