diff --git a/backend/handlers/scheduling/default-hours.go b/backend/handlers/scheduling/default-hours.go index fc56276..710bab7 100644 --- a/backend/handlers/scheduling/default-hours.go +++ b/backend/handlers/scheduling/default-hours.go @@ -318,7 +318,15 @@ func GetAvailableHours(w http.ResponseWriter, r *http.Request) { // Load bookings bookingRows, _ := db.DB.Query(r.Context(), ` - SELECT b.start_time, COALESCE(SUM(s.duration_minutes),0) as total_duration + SELECT + b.start_time, + COALESCE(SUM( + CASE + WHEN bs.override_duration_minutes IS NOT NULL AND bs.override_duration_minutes > 0 + THEN bs.override_duration_minutes + ELSE s.duration_minutes + END + ), 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 diff --git a/backend/handlers/services/services.go b/backend/handlers/services/services.go index 11ad5a3..e6d27df 100644 --- a/backend/handlers/services/services.go +++ b/backend/handlers/services/services.go @@ -22,9 +22,7 @@ type Service struct { PatchTestDurationHours int `json:"patch_test_duration_hours"` MinimumAgeRequired int `json:"minimum_age_required"` CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` CreatedBy *string `json:"created_by,omitempty"` - UpdatedBy *string `json:"updated_by,omitempty"` } type ServiceResponse struct { @@ -55,8 +53,8 @@ func ToggleService(w http.ResponseWriter, r *http.Request) { return } - query := "UPDATE services SET is_active = NOT is_active, updated_at = NOW(), updated_by = $1 WHERE id = $2" - result, err := db.DB.Exec(r.Context(), query, r.Context().Value(mw.UserIDKey), serviceID) + query := "UPDATE services SET is_active = NOT is_active WHERE id = $1" + result, err := db.DB.Exec(r.Context(), query, serviceID) if err != nil { http.Error(w, "Failed to toggle service: "+err.Error(), http.StatusInternalServerError) return @@ -112,17 +110,17 @@ func CreateServiceHandler(w http.ResponseWriter, r *http.Request) { query := ` INSERT INTO services ( name, description, price, duration_minutes, - patch_test_duration_hours, minimum_age_required, created_by, updated_by + patch_test_duration_hours, minimum_age_required, created_by ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, name, description, price, duration_minutes, is_active, patch_test_duration_hours, minimum_age_required, created_at, - updated_at, created_by, updated_by + created_by ` var service Service - var createdByDB, updatedByDB sql.NullString + var createdByDB sql.NullString err := db.DB.QueryRow(r.Context(), query, @@ -133,7 +131,6 @@ func CreateServiceHandler(w http.ResponseWriter, r *http.Request) { req.PatchTestDurationHours, req.MinimumAgeRequired, createdBy, - createdBy, // updated_by same as created_by for new records ).Scan( &service.ID, &service.Name, @@ -144,9 +141,7 @@ func CreateServiceHandler(w http.ResponseWriter, r *http.Request) { &service.PatchTestDurationHours, &service.MinimumAgeRequired, &service.CreatedAt, - &service.UpdatedAt, &createdByDB, - &updatedByDB, ) if err != nil { @@ -163,9 +158,6 @@ func CreateServiceHandler(w http.ResponseWriter, r *http.Request) { if createdByDB.Valid { service.CreatedBy = &createdByDB.String } - if updatedByDB.Valid { - service.UpdatedBy = &updatedByDB.String - } // Return created service w.Header().Set("Content-Type", "application/json") @@ -270,8 +262,7 @@ func AllServicesHandler(w http.ResponseWriter, r *http.Request) { // Query all services including inactive ones query := ` SELECT id, name, description, price, duration_minutes, is_active, - patch_test_duration_hours, minimum_age_required, created_at, - updated_at, created_by, updated_by + patch_test_duration_hours, minimum_age_required, created_at, created_by FROM services ORDER BY is_active DESC, name ` @@ -287,7 +278,7 @@ func AllServicesHandler(w http.ResponseWriter, r *http.Request) { for rows.Next() { var service Service - var createdBy, updatedBy sql.NullString + var createdBy sql.NullString err := rows.Scan( &service.ID, @@ -299,9 +290,7 @@ func AllServicesHandler(w http.ResponseWriter, r *http.Request) { &service.PatchTestDurationHours, &service.MinimumAgeRequired, &service.CreatedAt, - &service.UpdatedAt, &createdBy, - &updatedBy, ) if err != nil { http.Error(w, "Failed to read service data: "+err.Error(), http.StatusInternalServerError) @@ -312,9 +301,6 @@ func AllServicesHandler(w http.ResponseWriter, r *http.Request) { if createdBy.Valid { service.CreatedBy = &createdBy.String } - if updatedBy.Valid { - service.UpdatedBy = &updatedBy.String - } services = append(services, service) } diff --git a/init-scripts/init-script.sql b/init-scripts/init-script.sql index 20f95d2..0726537 100644 --- a/init-scripts/init-script.sql +++ b/init-scripts/init-script.sql @@ -14,7 +14,7 @@ CREATE TYPE account_role AS ENUM ('unverified_email', 'verified_email', 'admin', CREATE TYPE account_type AS ENUM ('email', 'google', 'microsoft', 'facebook', 'guest'); CREATE TYPE payment_type AS ENUM ('deposit', 'full', 'tip', 'balance', 'partial'); CREATE TYPE payment_method AS ENUM ('online_square', 'in_person_card', 'cash', 'giftcard', 'discount'); -CREATE TYPE booking_status AS ENUM ('pending', 'confirmed', 'in_progress', 'completed', 'cancelled', 'no_show'); +CREATE TYPE booking_status AS ENUM ('pending', 'confirmed', 'in_progress', 'completed', 'client_cancelled', 'we_cancelled', 're-schedule', 'no_show'); CREATE TYPE payment_status AS ENUM ('pending', 'completed', 'failed', 'refunded'); -- ======================================= @@ -105,7 +105,9 @@ CREATE TABLE users ( data_consent_updated_at TIMESTAMPTZ DEFAULT NOW(), -- Audit fields created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + -- staff fields + notes TEXT ); CREATE TABLE user_social_logins ( @@ -144,10 +146,10 @@ CREATE TABLE services ( is_active BOOLEAN NOT NULL DEFAULT TRUE, patch_test_duration_hours INT NOT NULL DEFAULT 0, minimum_age_required INT NOT NULL DEFAULT 16, + requires_manual_pricing BOOLEAN NOT NULL DEFAULT FALSE, + requires_manual_duration BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by CHAR(12), - updated_by CHAR(12) ); CREATE INDEX idx_services_name ON services(name); @@ -170,10 +172,10 @@ CREATE TABLE bookings ( user_id CHAR(12) REFERENCES users(id) ON DELETE SET NULL, start_time TIMESTAMPTZ NOT NULL, status booking_status NOT NULL DEFAULT 'pending', + notes TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by CHAR(12), - updated_by CHAR(12) ); CREATE INDEX idx_bookings_userid ON bookings(user_id); @@ -188,6 +190,8 @@ CREATE INDEX idx_bookings_userid_starttime ON bookings(user_id, start_time); CREATE TABLE booking_services ( booking_id CHAR(12) NOT NULL REFERENCES bookings(id) ON DELETE CASCADE, service_id CHAR(12) NOT NULL REFERENCES services(id) ON DELETE RESTRICT, + override_price NUMERIC(10,2), + override_duration_minutes INT, PRIMARY KEY (booking_id, service_id) ); @@ -257,7 +261,7 @@ CREATE SEQUENCE invoice_number_seq CREATE TABLE payments ( id CHAR(12) PRIMARY KEY DEFAULT generate_payment_id(), - booking_id CHAR(12) NOT NULL REFERENCES bookings(id) ON DELETE CASCADE, + booking_id CHAR(12) NOT NULL REFERENCES bookings(id) ON DELETE RESTRICT, payment_type payment_type NOT NULL, payment_method payment_method NOT NULL, vendor_code TEXT, @@ -272,7 +276,6 @@ CREATE TABLE payments ( created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by CHAR(12), - updated_by CHAR(12) ); CREATE INDEX idx_payments_bookingid ON payments(booking_id); diff --git a/local-dev.sh b/local-dev.sh index f61a6f7..c0b96fc 100755 --- a/local-dev.sh +++ b/local-dev.sh @@ -165,6 +165,84 @@ echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "✅ Successfully created: $SUCCESS_COUNT services" echo "❌ Failed: $FAIL_COUNT services" + +# --- 5️⃣ Create Holiday Exceptional Groups --- +echo "" +echo "5️⃣ Creating Holiday Exceptional Groups..." + +# November Break (week of Nov 10-16, 2025) - Closed entirely +NOVEMBER_BREAK='{ + "name": "November Break", + "description": "Short break period in November", + "hours": [ + {"weekday": 0, "startTime": "00:00:00", "endTime": "00:00:00", "isOpen": false}, + {"weekday": 1, "startTime": "00:00:00", "endTime": "00:00:00", "isOpen": false}, + {"weekday": 2, "startTime": "00:00:00", "endTime": "00:00:00", "isOpen": false}, + {"weekday": 3, "startTime": "00:00:00", "endTime": "00:00:00", "isOpen": false}, + {"weekday": 4, "startTime": "00:00:00", "endTime": "00:00:00", "isOpen": false}, + {"weekday": 5, "startTime": "00:00:00", "endTime": "00:00:00", "isOpen": false}, + {"weekday": 6, "startTime": "00:00:00", "endTime": "00:00:00", "isOpen": false} + ], + "weekStarts": ["2025-11-10"] +}' + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "Creating: November Break" +NOV_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ + -H 'Content-Type: application/json' \ + -H "Authorization: Bearer $TOKEN" \ + -d "$NOVEMBER_BREAK" \ + "$BASE_URL/scheduling/exceptional-groups") +HTTP_CODE=$(echo "$NOV_RESPONSE" | tail -n1) +RESPONSE_BODY=$(echo "$NOV_RESPONSE" | sed '$d') + +if [ "$HTTP_CODE" = "201" ]; then + echo "✅ Created: November Break" +else + echo "❌ Failed to create November Break (HTTP $HTTP_CODE)" + echo "Response: $RESPONSE_BODY" +fi + +sleep 0.2 + +# Christmas Holiday (weeks of Dec 22-28 and Dec 29-Jan 4) - Limited hours +CHRISTMAS_BREAK='{ + "name": "Christmas Holiday Period", + "description": "Reduced hours for Christmas and New Year", + "hours": [ + {"weekday": 0, "startTime": "00:00:00", "endTime": "00:00:00", "isOpen": false}, + {"weekday": 1, "startTime": "10:00:00", "endTime": "15:00:00", "isOpen": true}, + {"weekday": 2, "startTime": "10:00:00", "endTime": "15:00:00", "isOpen": true}, + {"weekday": 3, "startTime": "00:00:00", "endTime": "00:00:00", "isOpen": false}, + {"weekday": 4, "startTime": "00:00:00", "endTime": "00:00:00", "isOpen": false}, + {"weekday": 5, "startTime": "00:00:00", "endTime": "00:00:00", "isOpen": false}, + {"weekday": 6, "startTime": "00:00:00", "endTime": "00:00:00", "isOpen": false} + ], + "weekStarts": ["2025-12-22", "2025-12-29"] +}' + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "Creating: Christmas Holiday Period" +XMAS_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ + -H 'Content-Type: application/json' \ + -H "Authorization: Bearer $TOKEN" \ + -d "$CHRISTMAS_BREAK" \ + "$BASE_URL/scheduling/exceptional-groups") +HTTP_CODE=$(echo "$XMAS_RESPONSE" | tail -n1) +RESPONSE_BODY=$(echo "$XMAS_RESPONSE" | sed '$d') + +if [ "$HTTP_CODE" = "201" ]; then + echo "✅ Created: Christmas Holiday Period" +else + echo "❌ Failed to create Christmas Holiday Period (HTTP $HTTP_CODE)" + echo "Response: $RESPONSE_BODY" +fi + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "🎉 Seeding completed!" EOF chmod +x /tmp/seed_data.sh