Add exceptional hour modals
This commit is contained in:
@@ -3,6 +3,7 @@ package scheduling
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"crussell/db"
|
"crussell/db"
|
||||||
@@ -21,19 +22,14 @@ type ExceptionalGroup struct {
|
|||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
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 {
|
// --- List Groups with Hours and Applications ---
|
||||||
ID int `json:"id"`
|
|
||||||
GroupID int `json:"groupId"`
|
|
||||||
WeekStart string `json:"weekStart"` // week beginning monday the Xth
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Exceptional Groups & Hours ---
|
|
||||||
func ListExceptionalGroups(w http.ResponseWriter, r *http.Request) {
|
func ListExceptionalGroups(w http.ResponseWriter, r *http.Request) {
|
||||||
rows, err := db.DB.Query(r.Context(), `
|
rows, err := db.DB.Query(r.Context(), `
|
||||||
SELECT id,name,description
|
SELECT id, name, description
|
||||||
FROM exceptional_working_hours_groups
|
FROM exceptional_working_hours_groups
|
||||||
ORDER BY id DESC
|
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)
|
http.Error(w, "failed to scan group", http.StatusInternalServerError)
|
||||||
return
|
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
|
SELECT id, weekday, start_time::text, end_time::text, is_open
|
||||||
FROM exceptional_working_hours
|
FROM exceptional_working_hours
|
||||||
WHERE group_id=$1 ORDER BY weekday
|
WHERE group_id=$1 ORDER BY weekday
|
||||||
`, g.ID)
|
`, g.ID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "failed to fetch group hours", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
for hoursRows.Next() {
|
for hoursRows.Next() {
|
||||||
var h ExceptionalHours
|
var h ExceptionalHours
|
||||||
if err := hoursRows.Scan(&h.ID, &h.Weekday, &h.StartTime, &h.EndTime, &h.IsOpen); err == nil {
|
if err := hoursRows.Scan(&h.ID, &h.Weekday, &h.StartTime, &h.EndTime, &h.IsOpen); err != nil {
|
||||||
h.GroupID = g.ID
|
hoursRows.Close()
|
||||||
g.Hours = append(g.Hours, h)
|
http.Error(w, "failed to scan hours", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
h.GroupID = g.ID
|
||||||
|
g.Hours = append(g.Hours, h)
|
||||||
}
|
}
|
||||||
hoursRows.Close()
|
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)
|
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")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(groups)
|
json.NewEncoder(w).Encode(groups)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Create Group with Hours and Applications (bulk) ---
|
||||||
func CreateExceptionalGroup(w http.ResponseWriter, r *http.Request) {
|
func CreateExceptionalGroup(w http.ResponseWriter, r *http.Request) {
|
||||||
var g ExceptionalGroup
|
var g ExceptionalGroup
|
||||||
if err := json.NewDecoder(r.Body).Decode(&g); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&g); err != nil {
|
||||||
http.Error(w, "invalid payload", http.StatusBadRequest)
|
http.Error(w, "invalid payload", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(g.Hours) != 7 {
|
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
|
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())
|
tx, err := db.DB.Begin(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "failed to start tx", http.StatusInternalServerError)
|
http.Error(w, "failed to start transaction", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer tx.Rollback(r.Context())
|
defer tx.Rollback(r.Context())
|
||||||
|
|
||||||
|
// Create group
|
||||||
err = tx.QueryRow(r.Context(), `
|
err = tx.QueryRow(r.Context(), `
|
||||||
INSERT INTO exceptional_working_hours_groups (name, description)
|
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)
|
`, g.Name, g.Description).Scan(&g.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "failed to create group", http.StatusInternalServerError)
|
http.Error(w, "failed to create group", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, h := range g.Hours {
|
// Insert hours and collect their IDs
|
||||||
_, err := tx.Exec(r.Context(), `
|
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)
|
INSERT INTO exceptional_working_hours (group_id, weekday, start_time, end_time, is_open)
|
||||||
VALUES ($1,$2,$3,$4,$5)
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
`, g.ID, h.Weekday, h.StartTime, h.EndTime, h.IsOpen)
|
RETURNING id
|
||||||
|
`, g.ID, h.Weekday, h.StartTime, h.EndTime, h.IsOpen).Scan(&id)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Commit(r.Context()); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
json.NewEncoder(w).Encode(g)
|
json.NewEncoder(w).Encode(g)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Exceptional Applications (assign a group to a week) ---
|
// --- Delete Group (cascades to hours and applications) ---
|
||||||
func ListExceptionalApplications(w http.ResponseWriter, r *http.Request) {
|
func DeleteExceptionalGroup(w http.ResponseWriter, r *http.Request) {
|
||||||
rows, err := db.DB.Query(r.Context(), `
|
// Extract group ID from URL path or query params
|
||||||
SELECT id, group_id, week_start
|
// Assuming you have a router that provides this, e.g. chi, mux, etc.
|
||||||
FROM exceptional_group_applications
|
// For this example, we'll use query param: DELETE /exceptional-groups?id=123
|
||||||
ORDER BY week_start DESC
|
idStr := r.URL.Query().Get("id")
|
||||||
`)
|
if idStr == "" {
|
||||||
if err != nil {
|
http.Error(w, "missing id parameter", http.StatusBadRequest)
|
||||||
http.Error(w, "failed to fetch applications", http.StatusInternalServerError)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
var list []ExceptionalApplication
|
id, err := strconv.Atoi(idStr)
|
||||||
for rows.Next() {
|
if err != nil {
|
||||||
var a ExceptionalApplication
|
http.Error(w, "invalid id parameter", http.StatusBadRequest)
|
||||||
var weekStart time.Time
|
return
|
||||||
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")
|
result, err := db.DB.Exec(r.Context(), `
|
||||||
json.NewEncoder(w).Encode(list)
|
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) {
|
// --- Update Applied Weeks (replaces all applications for a group) ---
|
||||||
var a ExceptionalApplication
|
func UpdateExceptionalApplications(w http.ResponseWriter, r *http.Request) {
|
||||||
if err := json.NewDecoder(r.Body).Decode(&a); err != nil {
|
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)
|
http.Error(w, "invalid payload", http.StatusBadRequest)
|
||||||
return
|
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 {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = db.DB.QueryRow(r.Context(), `
|
// Insert new applications
|
||||||
INSERT INTO exceptional_group_applications (group_id, week_start)
|
for _, weekStart := range parsedWeeks {
|
||||||
VALUES ($1,$2)
|
_, err := tx.Exec(r.Context(), `
|
||||||
RETURNING id
|
INSERT INTO exceptional_group_applications (group_id, week_start)
|
||||||
`, a.GroupID, weekStart).Scan(&a.ID)
|
VALUES ($1, $2)
|
||||||
if err != nil {
|
`, req.GroupID, weekStart)
|
||||||
http.Error(w, "failed to create application", http.StatusInternalServerError)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.WriteHeader(http.StatusNoContent)
|
||||||
json.NewEncoder(w).Encode(a)
|
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-3
@@ -77,11 +77,11 @@ func main() {
|
|||||||
r.Post("/register", authHandlers.RegisterHandler)
|
r.Post("/register", authHandlers.RegisterHandler)
|
||||||
r.Post("/login", authHandlers.LoginHandler)
|
r.Post("/login", authHandlers.LoginHandler)
|
||||||
|
|
||||||
// --- Scheduling public GET routes ---
|
// --- Scheduling Routes ---
|
||||||
r.Route("/scheduling", func(r chi.Router) {
|
r.Route("/scheduling", func(r chi.Router) {
|
||||||
|
// Public GET routes
|
||||||
r.Get("/default-hours", scheduling.GetDefaultHours)
|
r.Get("/default-hours", scheduling.GetDefaultHours)
|
||||||
r.Get("/exceptional-groups", scheduling.ListExceptionalGroups)
|
r.Get("/exceptional-groups", scheduling.ListExceptionalGroups)
|
||||||
r.Get("/exceptional-applications", scheduling.ListExceptionalApplications)
|
|
||||||
r.Get("/working-hours", scheduling.GetWorkingHours)
|
r.Get("/working-hours", scheduling.GetWorkingHours)
|
||||||
r.Get("/available-hours", scheduling.GetAvailableHours)
|
r.Get("/available-hours", scheduling.GetAvailableHours)
|
||||||
|
|
||||||
@@ -92,7 +92,8 @@ func main() {
|
|||||||
|
|
||||||
r.Put("/default-hours", scheduling.UpdateDefaultHours)
|
r.Put("/default-hours", scheduling.UpdateDefaultHours)
|
||||||
r.Post("/exceptional-groups", scheduling.CreateExceptionalGroup)
|
r.Post("/exceptional-groups", scheduling.CreateExceptionalGroup)
|
||||||
r.Post("/exceptional-applications", scheduling.CreateExceptionalApplication)
|
r.Delete("/exceptional-groups", scheduling.DeleteExceptionalGroup)
|
||||||
|
r.Put("/exceptional-applications", scheduling.UpdateExceptionalApplications)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -178,43 +178,83 @@
|
|||||||
id?: number;
|
id?: number;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
week_starts: string[];
|
weekStarts: string[];
|
||||||
rows: WorkingHourRow[];
|
hours: WorkingHourRow[];
|
||||||
};
|
};
|
||||||
|
|
||||||
// DEMO DATA: Exception Groups
|
// Replace the demo data with empty array and add loading state
|
||||||
let exceptionGroups = $state<ExceptionGroup[]>([
|
let exceptionGroups = $state<ExceptionGroup[]>([]);
|
||||||
{
|
let exceptionGroupsLoading = $state(true);
|
||||||
id: 1,
|
|
||||||
name: 'Christmas Week',
|
// Add state for the exception modal
|
||||||
description: 'Closed from Mon-Wed, open reduced hours Thu/Fri',
|
let exceptionDraft = $state<ExceptionGroup>({
|
||||||
week_starts: ['2025-12-22'],
|
name: '',
|
||||||
rows: [
|
description: '',
|
||||||
{ weekday: 0, start_time: '09:00', end_time: '17:00', is_open: false },
|
weekStarts: [],
|
||||||
{ weekday: 1, start_time: '09:00', end_time: '17:00', is_open: false },
|
hours: [
|
||||||
{ weekday: 2, start_time: '09:00', end_time: '17:00', is_open: false },
|
{ weekday: 0, start_time: '00:00', end_time: '00:00', is_open: false },
|
||||||
{ weekday: 3, start_time: '10:00', end_time: '15:00', is_open: true },
|
{ weekday: 1, start_time: '09:00', end_time: '17:00', is_open: true },
|
||||||
{ weekday: 4, start_time: '10:00', end_time: '15:00', is_open: true },
|
{ weekday: 2, 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: 3, start_time: '09:00', end_time: '17:00', is_open: true },
|
||||||
{ weekday: 6, start_time: '09:00', end_time: '17:00', is_open: false }
|
{ 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 }
|
||||||
{
|
]
|
||||||
id: 2,
|
});
|
||||||
name: 'Summer Holiday',
|
|
||||||
description: 'Closed on Mondays/Tuesdays only',
|
let weekRangeFrom = $state('');
|
||||||
week_starts: ['2026-07-06', '2026-07-13', '2026-07-20'],
|
let weekRangeTo = $state('');
|
||||||
rows: [
|
|
||||||
{ weekday: 0, start_time: '09:00', end_time: '17:00', is_open: false },
|
async function fetchExceptionGroups() {
|
||||||
{ weekday: 1, start_time: '09:00', end_time: '17:00', is_open: false },
|
if (pageState !== 'authorized') return;
|
||||||
{ 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 },
|
exceptionGroupsLoading = true;
|
||||||
{ weekday: 4, start_time: '09:00', end_time: '17:00', is_open: true },
|
try {
|
||||||
{ weekday: 5, start_time: '09:00', end_time: '17:00', is_open: false },
|
const response = await fetch('/api/scheduling/exceptional-groups', {
|
||||||
{ weekday: 6, start_time: '09:00', end_time: '17:00', is_open: false }
|
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 loadingHours = $state(false);
|
||||||
let savingHours = $state(false);
|
let savingHours = $state(false);
|
||||||
@@ -283,24 +323,133 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function saveExceptionGroup() {
|
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 {
|
try {
|
||||||
await new Promise((r) => setTimeout(r, 500));
|
// Map to API format
|
||||||
exceptionGroups = exceptionGroups.filter((g) => g.id !== exceptionToDelete);
|
const payload = {
|
||||||
showDeleteExceptionAlert = false;
|
name: exceptionDraft.name,
|
||||||
exceptionToDelete = undefined;
|
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) {
|
} 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() {
|
function createNewException() {
|
||||||
|
resetExceptionForm();
|
||||||
showExceptionModal = true;
|
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 ===============
|
// =============== Users & Bookings ===============
|
||||||
type User = {
|
type User = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -464,8 +613,12 @@
|
|||||||
const to = new Date(toISO + 'T00:00:00');
|
const to = new Date(toISO + 'T00:00:00');
|
||||||
const first = new Date(from);
|
const first = new Date(from);
|
||||||
const day = first.getDay();
|
const day = first.getDay();
|
||||||
const mondayOffset = (day + 6) % 7;
|
const daysToMonday = day === 0 ? -6 : 1 - day;
|
||||||
first.setDate(first.getDate() - mondayOffset);
|
|
||||||
|
// 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)) {
|
for (let d = new Date(first); d <= to; d.setDate(d.getDate() + 7)) {
|
||||||
dest.push(isoDateOf(new Date(d)));
|
dest.push(isoDateOf(new Date(d)));
|
||||||
}
|
}
|
||||||
@@ -819,6 +972,8 @@
|
|||||||
let showSaveDefaultHoursAlert = $state(false);
|
let showSaveDefaultHoursAlert = $state(false);
|
||||||
let showDeleteExceptionAlert = $state(false);
|
let showDeleteExceptionAlert = $state(false);
|
||||||
let exceptionToDelete = $state<number | undefined>(undefined);
|
let exceptionToDelete = $state<number | undefined>(undefined);
|
||||||
|
let showViewExceptionModal = $state(false);
|
||||||
|
let viewingException = $state<ExceptionGroup | null>(null);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if pageState === 'loading'}
|
{#if pageState === 'loading'}
|
||||||
@@ -1130,52 +1285,52 @@
|
|||||||
<Button variant="default" onclick={createNewException}>New schedule</Button>
|
<Button variant="default" onclick={createNewException}>New schedule</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-4 space-y-3 md:grid-cols-2">
|
{#if exceptionGroupsLoading}
|
||||||
{#if exceptionGroups.length === 0}
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<p class="text-sm text-gray-500">No exception groups found.</p>
|
{#each Array(2) as _, i}
|
||||||
{/if}
|
<Skeleton class="h-32 w-full" />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
{#if exceptionGroups.length === 0}
|
||||||
|
<p class="col-span-2 text-sm text-gray-500">No exception groups found.</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#each exceptionGroups as g}
|
{#each exceptionGroups as g}
|
||||||
<div class="relative h-full rounded border p-3 pb-12">
|
<div class="relative h-full rounded border p-3 pb-12">
|
||||||
<!-- add bottom padding to avoid overlap -->
|
<div class="flex h-full flex-col gap-3">
|
||||||
<div class="flex h-full flex-col gap-3">
|
<div class="flex-1">
|
||||||
<div class="flex-1">
|
<div class="font-semibold">{g.name}</div>
|
||||||
<div class="font-semibold">{g.name}</div>
|
<div class="text-sm text-gray-600">{g.description}</div>
|
||||||
<div class="text-sm text-gray-600">{g.description}</div>
|
<div class="mt-1 text-xs text-gray-500">
|
||||||
<div class="mt-1 text-xs text-gray-500">
|
Applies to weeks: {g.weekStarts?.slice(0, 5).join(', ')}
|
||||||
Applies to weeks: {g.week_starts?.slice(0, 5).join(', ')}
|
{#if (g.weekStarts?.length ?? 0) > 5}
|
||||||
{#if (g.week_starts?.length ?? 0) > 5}
|
(+{(g.weekStarts?.length ?? 0) - 5} more)
|
||||||
(+{(g.week_starts?.length ?? 0) - 5} more)
|
{/if}
|
||||||
{/if}
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="absolute bottom-3 right-3 flex gap-2">
|
||||||
|
<Button variant="default" size="sm" onclick={() => openViewExceptionModal(g)}>
|
||||||
|
View
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onclick={() => {
|
||||||
|
exceptionToDelete = g.id;
|
||||||
|
showDeleteExceptionAlert = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Absolute positioned button -->
|
|
||||||
<div class="absolute bottom-3 right-3">
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
class="w-20"
|
|
||||||
onclick={() => {
|
|
||||||
alert('TODO - view group');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
View
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
class="w-20"
|
|
||||||
onclick={() => {
|
|
||||||
exceptionToDelete = g.id;
|
|
||||||
showDeleteExceptionAlert = true;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/each}
|
||||||
{/each}
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
|
||||||
@@ -1530,21 +1685,149 @@
|
|||||||
|
|
||||||
<!-- Exception Group Modal -->
|
<!-- Exception Group Modal -->
|
||||||
<Modal.Root bind:open={showExceptionModal}>
|
<Modal.Root bind:open={showExceptionModal}>
|
||||||
<Modal.Content class="max-h-[90vh] max-w-sm overflow-y-auto p-4 md:max-w-lg">
|
<Modal.Content class="max-h-[90vh] max-w-2xl overflow-y-auto">
|
||||||
<Modal.Header class="mb-4 p-0">
|
<Modal.Header>
|
||||||
<Modal.Title class="text-lg font-semibold">New Exception Group</Modal.Title>
|
<Modal.Title class="text-lg font-semibold">Create Exception Schedule</Modal.Title>
|
||||||
|
<Modal.Description>
|
||||||
|
Define custom working hours for holidays, closures, or special events.
|
||||||
|
</Modal.Description>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Footer class="flex items-center justify-end gap-2 p-0 pt-4">
|
|
||||||
|
<div class="space-y-6 px-4 pb-4">
|
||||||
|
<!-- Basic Info -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label for="exception-name" class="text-sm font-medium">Schedule Name *</label>
|
||||||
|
<Input
|
||||||
|
id="exception-name"
|
||||||
|
type="text"
|
||||||
|
placeholder="e.g., Christmas Week, Summer Holiday"
|
||||||
|
bind:value={exceptionDraft.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label for="exception-description" class="text-sm font-medium">Description</label>
|
||||||
|
<Input
|
||||||
|
id="exception-description"
|
||||||
|
type="text"
|
||||||
|
placeholder="Brief description of the service, will be shown to customers"
|
||||||
|
bind:value={exceptionDraft.description}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<!-- Week Selection -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="mb-2 text-sm font-medium">Apply to Weeks *</h3>
|
||||||
|
<p class="mb-3 text-xs text-gray-500">
|
||||||
|
Select a date range to add all Mondays within that range
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label for="week-from" class="text-xs text-gray-600">From Date</label>
|
||||||
|
<Input id="week-from" type="date" bind:value={weekRangeFrom} />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label for="week-to" class="text-xs text-gray-600">To Date</label>
|
||||||
|
<Input id="week-to" type="date" bind:value={weekRangeTo} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button variant="outline" size="sm" onclick={addWeekRange} class="mt-3">
|
||||||
|
Add Week Range
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if exceptionDraft.weekStarts.length > 0}
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="text-xs text-gray-600">
|
||||||
|
Selected weeks ({exceptionDraft.weekStarts.length}):
|
||||||
|
</div>
|
||||||
|
<div class="max-h-32 space-y-1 overflow-y-auto rounded border p-2">
|
||||||
|
{#each exceptionDraft.weekStarts as week, index}
|
||||||
|
<div class="flex items-center justify-between text-sm">
|
||||||
|
<span>Week starting: {week}</span>
|
||||||
|
<button
|
||||||
|
class="text-xs text-red-500 hover:text-red-700"
|
||||||
|
onclick={() => removeWeek(index)}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<!-- Working Hours -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
<h3 class="text-sm font-medium">Working Hours for these Weeks *</h3>
|
||||||
|
<div class="w-full overflow-x-auto">
|
||||||
|
<table class="w-full table-auto text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr class="text-left text-xs text-gray-500">
|
||||||
|
<th class="py-2">Day</th>
|
||||||
|
<th class="py-2">Open</th>
|
||||||
|
<th class="py-2">Start</th>
|
||||||
|
<th class="py-2">End</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each exceptionDraft.hours as row}
|
||||||
|
<tr class="border-t">
|
||||||
|
<td class="py-2">{weekdayLabel(row.weekday)}</td>
|
||||||
|
<td class="py-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={row.is_open}
|
||||||
|
class="h-4 w-4 rounded border-gray-300 bg-gray-100"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td class="py-2">
|
||||||
|
<Input
|
||||||
|
type="time"
|
||||||
|
bind:value={row.start_time}
|
||||||
|
disabled={!row.is_open}
|
||||||
|
class="w-24 text-sm"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td class="py-2">
|
||||||
|
<Input
|
||||||
|
type="time"
|
||||||
|
bind:value={row.end_time}
|
||||||
|
disabled={!row.is_open}
|
||||||
|
class="w-24 text-sm"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal.Footer class="flex items-center justify-end gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
showExceptionModal = false;
|
showExceptionModal = false;
|
||||||
|
resetExceptionForm();
|
||||||
}}
|
}}
|
||||||
|
disabled={savingHours}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onclick={saveExceptionGroup} disabled={savingHours}>
|
<Button onclick={saveExceptionGroup} disabled={savingHours}>
|
||||||
{savingHours ? 'Saving…' : 'Save'}
|
{savingHours ? 'Creating…' : 'Create Schedule'}
|
||||||
</Button>
|
</Button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</Modal.Content>
|
</Modal.Content>
|
||||||
@@ -1573,6 +1856,89 @@
|
|||||||
</AlertDialog.Content>
|
</AlertDialog.Content>
|
||||||
</AlertDialog.Root>
|
</AlertDialog.Root>
|
||||||
|
|
||||||
|
<!-- View Exception Modal -->
|
||||||
|
{#if viewingException}
|
||||||
|
<Modal.Root bind:open={showViewExceptionModal}>
|
||||||
|
<Modal.Content class="max-h-[90vh] max-w-2xl overflow-y-auto">
|
||||||
|
<Modal.Header>
|
||||||
|
<Modal.Title class="text-lg font-semibold">{viewingException.name}</Modal.Title>
|
||||||
|
<Modal.Description>
|
||||||
|
{viewingException.description || 'Holiday schedule details'}
|
||||||
|
</Modal.Description>
|
||||||
|
</Modal.Header>
|
||||||
|
|
||||||
|
<div class="space-y-6 px-4 pb-4">
|
||||||
|
<!-- Applied Weeks -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h3 class="text-sm font-medium">Applied to Weeks</h3>
|
||||||
|
<div class="max-h-48 space-y-1 overflow-y-auto rounded border bg-gray-50 p-3">
|
||||||
|
{#if viewingException.weekStarts && viewingException.weekStarts.length > 0}
|
||||||
|
<div class="grid grid-cols-2 gap-2 md:grid-cols-3">
|
||||||
|
{#each viewingException.weekStarts as week}
|
||||||
|
<div class="text-sm">
|
||||||
|
Week of {new Date(week).toLocaleDateString('en-GB', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric'
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<p class="text-sm text-gray-500">No weeks specified</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<!-- Working Hours -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
<h3 class="text-sm font-medium">Working Hours</h3>
|
||||||
|
<div class="w-full overflow-x-auto">
|
||||||
|
<table class="w-full table-auto">
|
||||||
|
<thead>
|
||||||
|
<tr class="text-left text-xs text-gray-500">
|
||||||
|
<th class="py-2">Day</th>
|
||||||
|
<th class="py-2">Status</th>
|
||||||
|
<th class="py-2">Start</th>
|
||||||
|
<th class="py-2">End</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each viewingException.hours as row}
|
||||||
|
<tr class="border-t">
|
||||||
|
<td class="py-2 text-sm">{weekdayLabel(row.weekday)}</td>
|
||||||
|
<td class="py-2">
|
||||||
|
<span
|
||||||
|
class="text-sm font-medium {row.is_open
|
||||||
|
? 'text-emerald-600'
|
||||||
|
: 'text-red-600'}"
|
||||||
|
>
|
||||||
|
{row.is_open ? 'Open' : 'Closed'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 text-sm">
|
||||||
|
{row.is_open ? row.start_time : '—'}
|
||||||
|
</td>
|
||||||
|
<td class="py-2 text-sm">
|
||||||
|
{row.is_open ? row.end_time : '—'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal.Footer class="flex items-center justify-end gap-2">
|
||||||
|
<Button onclick={() => (showViewExceptionModal = false)}>Close</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal.Content>
|
||||||
|
</Modal.Root>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- User Modal -->
|
<!-- User Modal -->
|
||||||
{#if selectedUser}
|
{#if selectedUser}
|
||||||
<Modal.Root bind:open={showUserModal}>
|
<Modal.Root bind:open={showUserModal}>
|
||||||
@@ -1669,7 +2035,7 @@
|
|||||||
<Input
|
<Input
|
||||||
id="service-description"
|
id="service-description"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Brief description of the service"
|
placeholder="Brief description of the service, will be shown to customers"
|
||||||
bind:value={newService.description}
|
bind:value={newService.description}
|
||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -302,7 +302,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if we have working hours data
|
// 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 dateStr = date.toString();
|
||||||
const dayHours = workingHours[dateStr];
|
const dayHours = workingHours[dateStr];
|
||||||
@@ -472,7 +472,13 @@
|
|||||||
const dayWorkingHours = workingHours[dateStr];
|
const dayWorkingHours = workingHours[dateStr];
|
||||||
const dayAvailableHours = availableHours[dateStr];
|
const dayAvailableHours = availableHours[dateStr];
|
||||||
|
|
||||||
if (!dayWorkingHours || !dayWorkingHours.isOpen || !dayAvailableHours) {
|
// Add check for slots existence
|
||||||
|
if (
|
||||||
|
!dayWorkingHours ||
|
||||||
|
!dayWorkingHours.isOpen ||
|
||||||
|
!dayAvailableHours ||
|
||||||
|
!dayAvailableHours.slots
|
||||||
|
) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user