From 29aaa7392ef197f2112721c3b39bfd21a05593de Mon Sep 17 00:00:00 2001 From: Stephen Adamson Date: Tue, 14 Oct 2025 22:09:49 +0100 Subject: [PATCH] Working default hours update --- backend/auth/jwt.go | 1 - backend/handlers/scheduling/default-hours.go | 392 ++++++ backend/main.go | 56 +- .../src/lib/components/ui/skeleton/index.ts | 7 + .../components/ui/skeleton/skeleton.svelte | 17 + frontend/src/routes/admin/+page.svelte | 1154 +++++++++-------- init-scripts/init-script.sql | 52 +- 7 files changed, 1128 insertions(+), 551 deletions(-) create mode 100644 backend/handlers/scheduling/default-hours.go create mode 100644 frontend/src/lib/components/ui/skeleton/index.ts create mode 100644 frontend/src/lib/components/ui/skeleton/skeleton.svelte diff --git a/backend/auth/jwt.go b/backend/auth/jwt.go index d9ec1d1..d31d3c2 100644 --- a/backend/auth/jwt.go +++ b/backend/auth/jwt.go @@ -1,4 +1,3 @@ -// auth/jwt.go package auth import ( diff --git a/backend/handlers/scheduling/default-hours.go b/backend/handlers/scheduling/default-hours.go new file mode 100644 index 0000000..ad7a916 --- /dev/null +++ b/backend/handlers/scheduling/default-hours.go @@ -0,0 +1,392 @@ +package scheduling + +import ( + "encoding/json" + "fmt" + "net/http" + "time" + + "crussell/db" +) + +// --- Types --- +type DefaultHours struct { + Weekday int `json:"weekday"` + StartTime string `json:"startTime"` + EndTime string `json:"endTime"` + IsOpen bool `json:"isOpen"` +} + +type ExceptionalHours struct { + ID int `json:"id"` + GroupID int `json:"groupId"` + Weekday int `json:"weekday"` + StartTime string `json:"startTime"` + EndTime string `json:"endTime"` + IsOpen bool `json:"isOpen"` +} + +type ExceptionalGroup struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Hours []ExceptionalHours `json:"hours,omitempty"` // 7 entries +} + +type ExceptionalApplication struct { + ID int `json:"id"` + GroupID int `json:"groupId"` + WeekStart string `json:"weekStart"` // Monday date +} + +type DayWorkingHours struct { + Date string `json:"date"` + Weekday int `json:"weekday"` + StartTime string `json:"startTime"` + EndTime string `json:"endTime"` + IsOpen bool `json:"isOpen"` + Source string `json:"source"` // "default" or "exceptional" +} + +// --- Default Hours Handlers --- +func GetDefaultHours(w http.ResponseWriter, r *http.Request) { + rows, err := db.DB.Query(r.Context(), ` + SELECT weekday, start_time::text, end_time::text, is_open + FROM working_hours ORDER BY weekday + `) + if err != nil { + http.Error(w, "failed to fetch default hours", http.StatusInternalServerError) + return + } + defer rows.Close() + + var hours []DefaultHours + for rows.Next() { + var h DefaultHours + if err := rows.Scan(&h.Weekday, &h.StartTime, &h.EndTime, &h.IsOpen); err != nil { + http.Error(w, "failed to scan default hours", http.StatusInternalServerError) + return + } + hours = append(hours, h) + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(hours) +} + +func UpdateDefaultHours(w http.ResponseWriter, r *http.Request) { + var hours []DefaultHours + if err := json.NewDecoder(r.Body).Decode(&hours); err != nil { + http.Error(w, "invalid payload", http.StatusBadRequest) + return + } + + tx, err := db.DB.Begin(r.Context()) + if err != nil { + http.Error(w, "failed to start tx", http.StatusInternalServerError) + return + } + defer tx.Rollback(r.Context()) + + for _, h := range hours { + _, err := tx.Exec(r.Context(), ` + UPDATE working_hours + SET start_time=$1,end_time=$2,is_open=$3 + WHERE weekday=$4 + `, h.StartTime, h.EndTime, h.IsOpen, h.Weekday) + if err != nil { + http.Error(w, "failed to update default hours", http.StatusInternalServerError) + return + } + } + + if err := tx.Commit(r.Context()); err != nil { + http.Error(w, "failed to commit", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +// --- Exceptional Groups & Hours --- +func ListExceptionalGroups(w http.ResponseWriter, r *http.Request) { + rows, err := db.DB.Query(r.Context(), ` + SELECT id,name,description + FROM exceptional_working_hours_groups + ORDER BY id DESC + `) + if err != nil { + http.Error(w, "failed to fetch groups", http.StatusInternalServerError) + return + } + defer rows.Close() + + var groups []ExceptionalGroup + for rows.Next() { + var g ExceptionalGroup + if err := rows.Scan(&g.ID, &g.Name, &g.Description); err != nil { + http.Error(w, "failed to scan group", http.StatusInternalServerError) + return + } + // load 7-day hours + hoursRows, _ := 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) + 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) + } + } + hoursRows.Close() + groups = append(groups, g) + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(groups) +} + +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) + return + } + + tx, err := db.DB.Begin(r.Context()) + if err != nil { + http.Error(w, "failed to start tx", http.StatusInternalServerError) + return + } + defer tx.Rollback(r.Context()) + + err = tx.QueryRow(r.Context(), ` + INSERT INTO exceptional_working_hours_groups (name, description) + 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 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) + if err != nil { + http.Error(w, "failed to insert group hours", http.StatusInternalServerError) + return + } + } + + if err := tx.Commit(r.Context()); err != nil { + http.Error(w, "failed to commit", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + 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) + 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) + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(list) +} + +func CreateExceptionalApplication(w http.ResponseWriter, r *http.Request) { + var a ExceptionalApplication + if err := json.NewDecoder(r.Body).Decode(&a); err != nil { + http.Error(w, "invalid payload", http.StatusBadRequest) + return + } + weekStart, err := time.Parse("2006-01-02", a.WeekStart) + if err != nil { + http.Error(w, "invalid week_start format, expected YYYY-MM-DD", http.StatusBadRequest) + 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) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(a) +} + +// --- GetWorkingHours (merged default + applied exceptions) --- +func GetWorkingHours(w http.ResponseWriter, r *http.Request) { + startStr := r.URL.Query().Get("start") + endStr := r.URL.Query().Get("end") + if startStr == "" || endStr == "" { + http.Error(w, "start and end query params required", http.StatusBadRequest) + return + } + start, err := time.Parse("2006-01-02", startStr) + if err != nil { + http.Error(w, "invalid start date", http.StatusBadRequest) + return + } + end, err := time.Parse("2006-01-02", endStr) + if err != nil { + http.Error(w, "invalid end date", http.StatusBadRequest) + return + } + + // 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 + `) + for defRows.Next() { + var d DefaultHours + if err := defRows.Scan(&d.Weekday, &d.StartTime, &d.EndTime, &d.IsOpen); err == nil { + defaultMap[d.Weekday] = d + } + } + defRows.Close() + + // Load exceptional applications 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 + type appEntry struct { + GroupID int + WeekStart time.Time + } + var apps []appEntry + for appRows.Next() { + var e appEntry + if err := appRows.Scan(&e.GroupID, &e.WeekStart); err == nil { + apps = append(apps, e) + } + } + 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 + 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) + rows, _ := db.DB.Query(r.Context(), query, args...) + for rows.Next() { + var h ExceptionalHours + if err := rows.Scan(&h.GroupID, &h.Weekday, &h.StartTime, &h.EndTime, &h.IsOpen); err == nil { + if _, ok := exHoursMap[h.GroupID]; !ok { + exHoursMap[h.GroupID] = map[int]ExceptionalHours{} + } + exHoursMap[h.GroupID][h.Weekday] = h + } + } + rows.Close() + } + + // Generate final result per day + var results []DayWorkingHours + 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 + } else { + weekday -= 1 // shift Monday=0 ... Sunday=6 + } + + // find applied group for this week + var applied *ExceptionalHours + weekStart := d.AddDate(0, 0, -weekday) // Monday of current week + for _, a := range apps { + if a.WeekStart.Equal(weekStart) { + if dayHours, ok := exHoursMap[a.GroupID][weekday]; ok { + applied = &dayHours + } + break + } + } + + var day DayWorkingHours + day.Date = d.Format("2006-01-02") + day.Weekday = weekday + + if applied != nil { + day.StartTime = applied.StartTime + day.EndTime = applied.EndTime + day.IsOpen = applied.IsOpen + day.Source = "exceptional" + } else if def, ok := defaultMap[weekday]; ok { + day.StartTime = def.StartTime + day.EndTime = def.EndTime + day.IsOpen = def.IsOpen + day.Source = "default" + } else { + day.StartTime = "00:00" + day.EndTime = "00:00" + day.IsOpen = false + day.Source = "default" + } + + results = append(results, day) + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(results) +} + +// --- helper: sqlIn generates IN queries dynamically for Postgres --- +func sqlIn(query string, args []int) (string, []interface{}, error) { + inArgs := []interface{}{} + placeholders := "" + for i, arg := range args { + if i > 0 { + placeholders += "," + } + placeholders += fmt.Sprintf("$%d", i+1) + inArgs = append(inArgs, arg) + } + query = fmt.Sprintf(query, placeholders) + return query, inArgs, nil +} diff --git a/backend/main.go b/backend/main.go index f9fa87f..4d3722a 100644 --- a/backend/main.go +++ b/backend/main.go @@ -16,7 +16,8 @@ import ( "crussell/mw" authHandlers "crussell/handlers/auth" - userHandlers "crussell/handlers/user" + "crussell/handlers/scheduling" + "crussell/handlers/user" ) func init() { @@ -52,6 +53,7 @@ func main() { r := chi.NewRouter() + // --- Middleware --- r.Use(middleware.RequestID) // Add X-Request-ID header r.Use(middleware.RealIP) // Get real IP from headers r.Use(middleware.Logger) // Basic logging @@ -67,40 +69,52 @@ func main() { }) }) - // Public auth routes + // --- Public auth routes --- r.Post("/api/register", authHandlers.RegisterHandler) r.Post("/api/login", authHandlers.LoginHandler) - // Protected routes - any authenticated user + // --- Protected routes - any authenticated user --- r.Group(func(r chi.Router) { r.Use(mw.RequireAuth) - // Auth - r.Post("/api/refresh-token", authHandlers.RefreshTokenHandler) - // User profile - r.Get("/api/user/profile", userHandlers.GetProfileHandler) - r.Put("/api/user/profile", userHandlers.UpdateProfileHandler) - r.Delete("/api/user/account", userHandlers.DeleteAccountHandler) + r.Get("/api/user/profile", user.GetProfileHandler) + r.Put("/api/user/profile", user.UpdateProfileHandler) + r.Delete("/api/user/account", user.DeleteAccountHandler) // Loyalty - r.Get("/api/user/loyalty", userHandlers.GetLoyaltyHandler) + r.Get("/api/user/loyalty", user.GetLoyaltyHandler) }) - // Protected routes - verified users only - r.Group(func(r chi.Router) { - r.Use(mw.RequireAuth) - r.Use(mw.RequireVerified) + // --- Scheduling routes --- + r.Route("/api/scheduling", func(r chi.Router) { - // Add booking routes, etc. - }) + // Default hours + r.Get("/default-hours", scheduling.GetDefaultHours) + r.Group(func(r chi.Router) { + r.Use(mw.RequireAuth) + r.Use(mw.RequireAdmin) + r.Put("/default-hours", scheduling.UpdateDefaultHours) + }) - // Admin routes - r.Group(func(r chi.Router) { - r.Use(mw.RequireAuth) - r.Use(mw.RequireAdmin) + // Exceptional groups + r.Get("/exceptional-groups", scheduling.ListExceptionalGroups) + r.Group(func(r chi.Router) { + r.Use(mw.RequireAuth) + r.Use(mw.RequireAdmin) + r.Post("/exceptional-groups", scheduling.CreateExceptionalGroup) + }) - // Add admin routes + // Exceptional applications (assign groups to weeks) + r.Get("/exceptional-applications", scheduling.ListExceptionalApplications) + r.Group(func(r chi.Router) { + r.Use(mw.RequireAuth) + r.Use(mw.RequireAdmin) + r.Post("/exceptional-applications", scheduling.CreateExceptionalApplication) + }) + + // Merged working hours by date range + r.Get("/working-hours", scheduling.GetWorkingHours) }) fmt.Println("Server is listening on :8080") diff --git a/frontend/src/lib/components/ui/skeleton/index.ts b/frontend/src/lib/components/ui/skeleton/index.ts new file mode 100644 index 0000000..186db21 --- /dev/null +++ b/frontend/src/lib/components/ui/skeleton/index.ts @@ -0,0 +1,7 @@ +import Root from "./skeleton.svelte"; + +export { + Root, + // + Root as Skeleton, +}; diff --git a/frontend/src/lib/components/ui/skeleton/skeleton.svelte b/frontend/src/lib/components/ui/skeleton/skeleton.svelte new file mode 100644 index 0000000..c7e3d26 --- /dev/null +++ b/frontend/src/lib/components/ui/skeleton/skeleton.svelte @@ -0,0 +1,17 @@ + + +
diff --git a/frontend/src/routes/admin/+page.svelte b/frontend/src/routes/admin/+page.svelte index 8f82bc5..92d965c 100644 --- a/frontend/src/routes/admin/+page.svelte +++ b/frontend/src/routes/admin/+page.svelte @@ -13,14 +13,18 @@ // Custom component import FileDropZone from '$lib/components/ui/file-drop-zone.svelte'; + import { browser } from '$app/environment'; + import { toast } from 'svelte-sonner'; + import { Skeleton } from '$lib/components/ui/skeleton'; - // Auth guard - onMount(() => { - const user = authStore.currentUser; + const user = authStore.currentUser; + if (browser) { if (!user || user.role !== 'admin') { - goto('/', { replaceState: true }); + setTimeout(() => { + goto('/', { replaceState: true }); + }, 5000); } - }); + } // =============== Image Upload =============== let uploading = $state(false); @@ -72,14 +76,61 @@ is_open: boolean; }; - let defaultHours = $state( // Initial default values - Array.from({ length: 7 }, (_, i) => ({ - weekday: i, - start_time: '09:00', - end_time: '17:00', - is_open: i >= 0 && i <= 4 // Mon-Fri default open - })) - ); + let defaultHours = $state([]); + let defaultHoursIsLoading = $state(true); + + /** Format time from HH:MM:SS to 12-hour format */ + /** Format time from HH:MM:SS to 12-hour format, with "Noon" for 12:00 PM */ + function formatTime(time: string): string { + const [hours, minutes] = time.split(':').map(Number); + + // Special case for 12:00 PM + if (hours === 12 && minutes === 0) { + return 'Noon'; + } else if (hours === 0 && minutes === 0) { + return 'Midnight'; + } + + const period = hours >= 12 ? 'PM' : 'AM'; + const displayHours = hours % 12 || 12; + return `${displayHours}:${minutes.toString().padStart(2, '0')} ${period}`; + } + + async function fetchDefaultHours() { + defaultHoursIsLoading = true; + let error = null; + + try { + const response = await fetch('/api/scheduling/default-hours', { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + if (response.ok) { + const data = await response.json(); + defaultHours = data.map((hour: any) => ({ + weekday: hour.weekday, + start_time: formatTime(hour.startTime), + end_time: formatTime(hour.endTime), + is_open: hour.isOpen + })); + } else { + const text = await response.text(); + error = 'Failed to load working hours: ' + text; + console.error('Error fetching default hours:', text); + } + } catch (err) { + error = 'Network error: ' + (err instanceof Error ? err.message : 'Unknown error'); + console.error('Error fetching default hours:', err); + } finally { + if (error) toast.error(error); + defaultHoursIsLoading = false; + } + } + + $effect(() => { + fetchDefaultHours(); + }); type ExceptionGroup = { id?: number; @@ -163,19 +214,43 @@ return; savingHours = true; - try { - // Mock API call to save defaults - await new Promise((r) => setTimeout(r, 1000)); + const loadingToast = toast.loading('Saving default hours...'); - // Update the main state from the draft state if successful - defaultHours = defaultHoursDraft; - showDefaultHoursModal = false; - alert('Default hours saved successfully!'); + try { + // Map snake_case to camelCase for API + const payload = defaultHoursDraft.map((hour) => ({ + weekday: hour.weekday, + startTime: hour.start_time, + endTime: hour.end_time, + isOpen: hour.is_open + })); + + const response = await fetch('/api/scheduling/default-hours', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${localStorage.getItem('authToken')}` + }, + body: JSON.stringify(payload) + }); + + if (response.ok) { + // Update the main state from the draft state if successful + defaultHours = JSON.parse(JSON.stringify(defaultHoursDraft)); + showDefaultHoursModal = false; + toast.success('Default hours saved successfully!', { id: loadingToast }); + } else if (response.status === 401 || response.status === 403) { + toast.error('Unauthorized. Please log in again.', { id: loadingToast }); + } else { + const text = await response.text(); + toast.error('Failed to save: ' + text, { id: loadingToast }); + } } catch (err) { console.error('save default hours', err); - alert('Failed to save default hours.'); + toast.error('Network error saving hours', { id: loadingToast }); + } finally { + savingHours = false; } - savingHours = false; } async function saveExceptionGroup() { @@ -404,537 +479,588 @@ } -
-
-
-

Admin Dashboard

-

Manage portfolio images, working hours & user bookings

+{#if user?.role == 'admin'} +
+
+
+

Admin Dashboard

+

Manage portfolio images, working hours & user bookings

+
-
- - - Image Upload - Upload images for the portfolio or other uses. - - - -
-

Drop files here, or click to open the file picker

-
-
+ + + Image Upload + Upload images for the portfolio or other uses. + + + +
+

Drop files here, or click to open the file picker

+
+
-
-
Selected files ({uploadFiles.length})
-
- {#each uploadFiles as f} -
-
{f.name} • {Math.round(f.size / 1024)}KB
- -
- {/each} -
- {#if uploadResults.length > 0} -
Upload Results
+
+
Selected files ({uploadFiles.length})
- {#each uploadResults as result} -
- {result.name}: {result.error ? `Failed: ${result.error}` : `Success: ${result.url}`} -
- {/each} -
- {/if} -
- -
- -
- - - - - - - Working Hours - View and manage your standard weekly schedule. - - - -
-

Weekly Schedule

- -
-
- - - - - - - - - - - {#each defaultHours as row} - - - - - - - {/each} - -
DayStatusStartEnd
{weekdayLabel(row.weekday)} - - {row.is_open ? 'Open' : 'Closed'} - - - {row.is_open ? row.start_time : '—'} - - {row.is_open ? row.end_time : '—'} -
-
-
-
- - - - - Exception Groups - - Manage temporary schedules for holidays, closures, and special events. - - - - -
- -
- -
- {#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} -
-
- -
- - + Remove +
+ {/each} +
+ {#if uploadResults.length > 0} +
Upload Results
+
+ {#each uploadResults as result} +
+ {result.name}: {result.error + ? `Failed: ${result.error}` + : `Success: ${result.url}`} +
+ {/each} +
+ {/if} +
+ +
+ +
+ + + +
+ + + Users + Search and manage user details. + + +
+
+ { + if ((e as KeyboardEvent).key === 'Enter') searchUsers(); + }} + /> + +
+ +
+ {#each users as u} +
+
+
{u.fn || `${u.n_first_name} ${u.n_last_name}`}
+
{u.email} • {u.phone}
+
+ +
+ {/each}
- {/each} -
- - + + - - - Users - Search and manage user details. - - -
-
- { - if ((e as KeyboardEvent).key === 'Enter') searchUsers(); - }} - /> - -
- -
- {#each users as u} -
-
-
{u.fn || `${u.n_first_name} ${u.n_last_name}`}
-
{u.email} • {u.phone}
-
- + + + Bookings + Search and manage booking history. + + +
+
+ { + if ((e as KeyboardEvent).key === 'Enter') searchBookings(); + }} + /> +
- {/each} -
-
- - - - - Bookings - Search and manage booking history. - - -
-
- { - if ((e as KeyboardEvent).key === 'Enter') searchBookings(); - }} - /> - -
- -
- {#each bookings as b} -
-
-
{new Date(b.start_time).toLocaleString()}
-
- {b.status} • {b.services?.map((s) => s.name).join(', ')} +
+ {#each bookings as b} +
+
+
{new Date(b.start_time).toLocaleString()}
+
+ {b.status} • {b.services?.map((s) => s.name).join(', ')} +
+
+
-
- + {/each}
- {/each} -
-
- - -
- - - - - Edit Default Working Hours - - Set the standard open and close times for your business. - - - -
-
- - - - - - - - - - - {#each defaultHoursDraft as row} - - - - - - - {/each} - -
DayOpenStartEnd
{weekdayLabel(row.weekday)} - - - - - -
-
+
+ +
- - - - - - + + + Holiday Hours + + Manage temporary schedules for holidays, closures, and special events. + + - - - - - {editingExceptionIndex === null ? 'New Exception Group' : 'Edit Exception Group'} - - - - {#if editingException} -
-
- - -
-
- -