package api import ( "encoding/json" "net/http" "strconv" "strings" "time" "reservation-system/backend/internal/auth" "reservation-system/backend/internal/db" "reservation-system/backend/internal/models" "golang.org/x/crypto/bcrypt" ) type Handler struct { db *db.DB } func NewHandler(database *db.DB) *Handler { return &Handler{db: database} } func (h *Handler) RegisterRoutes(mux *http.ServeMux) { // Auth mux.HandleFunc("/api/auth/login", h.handleLogin) mux.HandleFunc("/api/auth/refresh", h.handleRefresh) // Rooms (protected) mux.HandleFunc("/api/rooms", h.authMiddleware(h.handleRooms)) mux.HandleFunc("/api/rooms/", h.authMiddleware(h.handleRoomDetail)) // Tables (protected) mux.HandleFunc("/api/tables", h.authMiddleware(h.handleTables)) mux.HandleFunc("/api/tables/", h.authMiddleware(h.handleTableDetail)) mux.HandleFunc("/api/tables/merge", h.authMiddleware(h.handleMergeTables)) // NEW: Room tables endpoint mux.HandleFunc("/api/rooms/", h.authMiddleware(h.handleRoomTables)) // Reservations (protected) mux.HandleFunc("/api/reservations", h.authMiddleware(h.handleReservations)) mux.HandleFunc("/api/reservations/", h.authMiddleware(h.handleReservationDetail)) mux.HandleFunc("/api/reservations/date/", h.authMiddleware(h.handleReservationsByDate)) // NEW: Room Bookings mux.HandleFunc("/api/room-bookings", h.authMiddleware(h.handleRoomBookings)) mux.HandleFunc("/api/room-bookings/", h.authMiddleware(h.handleRoomBookingDetail)) // Availability mux.HandleFunc("/api/availability", h.authMiddleware(h.handleAvailability)) mux.HandleFunc("/api/availability/room", h.authMiddleware(h.handleRoomAvailability)) // Email config mux.HandleFunc("/api/email-config", h.authMiddleware(h.handleEmailConfig)) // Dashboard stats mux.HandleFunc("/api/dashboard", h.authMiddleware(h.handleDashboard)) } func (h *Handler) handleLogin(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var req struct { Username string `json:"username"` Password string `json:"password"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request", http.StatusBadRequest) return } user, err := h.db.GetUserByUsername(req.Username) if err != nil { http.Error(w, "Server error", http.StatusInternalServerError) return } if user == nil || bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)) != nil { http.Error(w, "Invalid credentials", http.StatusUnauthorized) return } token, err := auth.GenerateToken(user.Username, user.IsAdmin) if err != nil { http.Error(w, "Server error", http.StatusInternalServerError) return } json.NewEncoder(w).Encode(map[string]string{ "token": token, "user": user.Username, }) } func (h *Handler) handleRefresh(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } authHeader := r.Header.Get("Authorization") if authHeader == "" { http.Error(w, "Missing token", http.StatusUnauthorized) return } tokenString := strings.TrimPrefix(authHeader, "Bearer ") claims, err := auth.ValidateToken(tokenString) if err != nil { http.Error(w, "Invalid token", http.StatusUnauthorized) return } newToken, err := auth.GenerateToken(claims.Username, claims.IsAdmin) if err != nil { http.Error(w, "Server error", http.StatusInternalServerError) return } json.NewEncoder(w).Encode(map[string]string{ "token": newToken, }) } func (h *Handler) authMiddleware(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { authHeader := r.Header.Get("Authorization") if authHeader == "" { http.Error(w, "Missing token", http.StatusUnauthorized) return } tokenString := strings.TrimPrefix(authHeader, "Bearer ") if _, err := auth.ValidateToken(tokenString); err != nil { http.Error(w, "Invalid token", http.StatusUnauthorized) return } next(w, r) } } // Rooms Handler func (h *Handler) handleRooms(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: rooms, err := h.db.GetRooms() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } json.NewEncoder(w).Encode(rooms) case http.MethodPost: var room models.Room if err := json.NewDecoder(r.Body).Decode(&room); err != nil { http.Error(w, "Invalid request", http.StatusBadRequest) return } if room.Name == "" { http.Error(w, "Name required", http.StatusBadRequest) return } if err := h.db.CreateRoom(&room); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(room) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } func (h *Handler) handleRoomDetail(w http.ResponseWriter, r *http.Request) { // Check if it's /api/rooms/:id/tables path := r.URL.Path if strings.HasSuffix(path, "/tables") { h.handleRoomTables(w, r) return } idStr := strings.TrimPrefix(path, "/api/rooms/") id, err := strconv.Atoi(idStr) if err != nil { http.Error(w, "Invalid ID", http.StatusBadRequest) return } switch r.Method { case http.MethodGet: room, err := h.db.GetRoom(id) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if room == nil { http.Error(w, "Not found", http.StatusNotFound) return } json.NewEncoder(w).Encode(room) case http.MethodPut: var room models.Room if err := json.NewDecoder(r.Body).Decode(&room); err != nil { http.Error(w, "Invalid request", http.StatusBadRequest) return } room.ID = id if err := h.db.UpdateRoom(&room); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } json.NewEncoder(w).Encode(room) case http.MethodDelete: if err := h.db.DeleteRoom(id); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } // NEW: Room Tables Handler - GET /api/rooms/:id/tables func (h *Handler) handleRoomTables(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet && r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } path := r.URL.Path idStr := strings.TrimSuffix(strings.TrimPrefix(path, "/api/rooms/"), "/tables") roomID, err := strconv.Atoi(idStr) if err != nil { http.Error(w, "Invalid room ID", http.StatusBadRequest) return } if r.Method == http.MethodGet { tables, err := h.db.GetTablesByRoom(roomID) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } json.NewEncoder(w).Encode(tables) } else { // POST - create table for room var table models.Table if err := json.NewDecoder(r.Body).Decode(&table); err != nil { http.Error(w, "Invalid request", http.StatusBadRequest) return } table.RoomID = roomID if table.MaxGuests == 0 { http.Error(w, "Max guests required", http.StatusBadRequest) return } if err := h.db.CreateTable(&table); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(table) } } // Tables Handler func (h *Handler) handleTables(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: roomID := r.URL.Query().Get("room_id") var tables []models.Table var err error if roomID != "" { rid, _ := strconv.Atoi(roomID) tables, err = h.db.GetTablesByRoom(rid) } else { tables, err = h.db.GetAllTables() } if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } json.NewEncoder(w).Encode(tables) case http.MethodPost: var table models.Table if err := json.NewDecoder(r.Body).Decode(&table); err != nil { http.Error(w, "Invalid request", http.StatusBadRequest) return } if table.MaxGuests == 0 { http.Error(w, "Max guests required", http.StatusBadRequest) return } if err := h.db.CreateTable(&table); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(table) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } func (h *Handler) handleTableDetail(w http.ResponseWriter, r *http.Request) { idStr := strings.TrimPrefix(r.URL.Path, "/api/tables/") id, err := strconv.Atoi(idStr) if err != nil { http.Error(w, "Invalid ID", http.StatusBadRequest) return } switch r.Method { case http.MethodGet: table, err := h.db.GetTable(id) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if table == nil { http.Error(w, "Not found", http.StatusNotFound) return } json.NewEncoder(w).Encode(table) case http.MethodPut: var table models.Table if err := json.NewDecoder(r.Body).Decode(&table); err != nil { http.Error(w, "Invalid request", http.StatusBadRequest) return } table.ID = id if err := h.db.UpdateTable(&table); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } json.NewEncoder(w).Encode(table) case http.MethodDelete: if err := h.db.DeleteTable(id); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } // Reservations Handler func (h *Handler) handleReservations(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: date := r.URL.Query().Get("date") if date == "" { date = time.Now().Format("2006-01-02") } reservations, err := h.db.GetReservationsByDate(date) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } json.NewEncoder(w).Encode(reservations) case http.MethodPost: var res models.Reservation if err := json.NewDecoder(r.Body).Decode(&res); err != nil { http.Error(w, "Invalid request", http.StatusBadRequest) return } // Check availability available, err := h.db.CheckAvailability(res.TableID, res.Date, res.TimeFrom, res.TimeTo) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if !available { http.Error(w, "Table not available at this time", http.StatusConflict) return } // Check table capacity table, err := h.db.GetTable(res.TableID) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if table != nil && res.Guests > table.MaxGuests { http.Error(w, "Guest count exceeds table capacity", http.StatusBadRequest) return } if res.Source == "" { res.Source = "manual" } if err := h.db.CreateReservation(&res); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(res) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } func (h *Handler) handleReservationDetail(w http.ResponseWriter, r *http.Request) { idStr := strings.TrimPrefix(r.URL.Path, "/api/reservations/") id, err := strconv.Atoi(idStr) if err != nil { http.Error(w, "Invalid ID", http.StatusBadRequest) return } switch r.Method { case http.MethodGet: res, err := h.db.GetReservation(id) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if res == nil { http.Error(w, "Not found", http.StatusNotFound) return } json.NewEncoder(w).Encode(res) case http.MethodPut: var res models.Reservation if err := json.NewDecoder(r.Body).Decode(&res); err != nil { http.Error(w, "Invalid request", http.StatusBadRequest) return } res.ID = id if err := h.db.UpdateReservation(&res); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } json.NewEncoder(w).Encode(res) case http.MethodDelete: if err := h.db.DeleteReservation(id); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } func (h *Handler) handleReservationsByDate(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } date := strings.TrimPrefix(r.URL.Path, "/api/reservations/date/") if date == "" { http.Error(w, "Date required", http.StatusBadRequest) return } reservations, err := h.db.GetReservationsByDate(date) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } json.NewEncoder(w).Encode(reservations) } // NEW: Room Bookings Handlers func (h *Handler) handleRoomBookings(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: roomID := r.URL.Query().Get("room_id") date := r.URL.Query().Get("date") var bookings []models.RoomBooking var err error if roomID != "" { rid, _ := strconv.Atoi(roomID) bookings, err = h.db.GetRoomBookingsByRoom(rid) } else if date != "" { bookings, err = h.db.GetRoomBookingsByDate(date) } else { // Get all bookings for today bookings, err = h.db.GetRoomBookingsByDate(time.Now().Format("2006-01-02")) } if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } json.NewEncoder(w).Encode(bookings) case http.MethodPost: var rb models.RoomBooking if err := json.NewDecoder(r.Body).Decode(&rb); err != nil { http.Error(w, "Invalid request", http.StatusBadRequest) return } // Check room availability available, err := h.db.CheckRoomAvailability(rb.RoomID, rb.Date, rb.TimeFrom, rb.TimeTo) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if !available { http.Error(w, "Room not available at this time", http.StatusConflict) return } // Check room capacity room, err := h.db.GetRoom(rb.RoomID) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if room != nil && rb.Guests > room.Capacity { http.Error(w, "Guest count exceeds room capacity", http.StatusBadRequest) return } if rb.Status == "" { rb.Status = "confirmed" } if err := h.db.CreateRoomBooking(&rb); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(rb) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } func (h *Handler) handleRoomBookingDetail(w http.ResponseWriter, r *http.Request) { idStr := strings.TrimPrefix(r.URL.Path, "/api/room-bookings/") id, err := strconv.Atoi(idStr) if err != nil { http.Error(w, "Invalid ID", http.StatusBadRequest) return } switch r.Method { case http.MethodGet: rb, err := h.db.GetRoomBooking(id) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if rb == nil { http.Error(w, "Not found", http.StatusNotFound) return } json.NewEncoder(w).Encode(rb) case http.MethodPut: var rb models.RoomBooking if err := json.NewDecoder(r.Body).Decode(&rb); err != nil { http.Error(w, "Invalid request", http.StatusBadRequest) return } rb.ID = id if err := h.db.UpdateRoomBooking(&rb); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } json.NewEncoder(w).Encode(rb) case http.MethodDelete: if err := h.db.DeleteRoomBooking(id); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } // Availability Handler func (h *Handler) handleAvailability(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } date := r.URL.Query().Get("date") if date == "" { date = time.Now().Format("2006-01-02") } tables, err := h.db.GetAllTables() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } var availability []map[string]interface{} for _, table := range tables { reservations, _ := h.db.GetReservationsByTable(table.ID, date) availability = append(availability, map[string]interface{}{ "table_id": table.ID, "table_name": table.RoomName + " - Tisch " + strconv.Itoa(table.ID), "max_guests": table.MaxGuests, "reservations": reservations, }) } json.NewEncoder(w).Encode(availability) } // NEW: Room Availability Handler func (h *Handler) handleRoomAvailability(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } date := r.URL.Query().Get("date") if date == "" { date = time.Now().Format("2006-01-02") } rooms, err := h.db.GetRooms() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } var availability []map[string]interface{} for _, room := range rooms { bookings, _ := h.db.GetRoomBookingsByRoom(room.ID) availability = append(availability, map[string]interface{}{ "room_id": room.ID, "room_name": room.Name, "capacity": room.Capacity, "bookings": bookings, }) } json.NewEncoder(w).Encode(availability) } func (h *Handler) handleMergeTables(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var req struct { ParentTableID int `json:"parent_table_id"` ChildTableID int `json:"child_table_id"` MergedName string `json:"merged_name"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request", http.StatusBadRequest) return } parentTable, err := h.db.GetTable(req.ParentTableID) if err != nil || parentTable == nil { http.Error(w, "Parent table not found", http.StatusBadRequest) return } childTable, err := h.db.GetTable(req.ChildTableID) if err != nil || childTable == nil { http.Error(w, "Child table not found", http.StatusBadRequest) return } if parentTable.RoomID != childTable.RoomID { http.Error(w, "Tables must be in the same room", http.StatusBadRequest) return } merge := models.TableMerge{ ParentTableID: req.ParentTableID, ChildTableID: req.ChildTableID, MergedName: req.MergedName, Active: true, } if err := h.db.CreateTableMerge(&merge); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(merge) } func (h *Handler) handleEmailConfig(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: cfg, err := h.db.GetEmailConfig() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if cfg != nil { cfg.Password = "***" } json.NewEncoder(w).Encode(cfg) case http.MethodPost, http.MethodPut: var cfg models.EmailConfig if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil { http.Error(w, "Invalid request", http.StatusBadRequest) return } if err := h.db.SaveEmailConfig(&cfg); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } json.NewEncoder(w).Encode(cfg) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } func (h *Handler) handleDashboard(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } today := time.Now().Format("2006-01-02") totalGuests, _ := h.db.GetTotalGuestsForDate(today) reservations, _ := h.db.GetReservationsByDate(today) roomBookings, _ := h.db.GetRoomBookingsByDate(today) dashboard := map[string]interface{}{ "today": today, "total_guests": totalGuests, "reservation_count": len(reservations), "room_booking_count": len(roomBookings), "reservations": reservations, "room_bookings": roomBookings, } json.NewEncoder(w).Encode(dashboard) }