v2.0: 3-Raum-System - Hauptraum, Saal A, Saal B mit 18 Tischen, Raum-Buchungen, API-Doku
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"reservation-system/backend/internal/api"
|
||||
"reservation-system/backend/internal/auth"
|
||||
"reservation-system/backend/internal/db"
|
||||
"reservation-system/backend/internal/email"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Initialize JWT
|
||||
jwtSecret := os.Getenv("JWT_SECRET")
|
||||
if jwtSecret == "" {
|
||||
jwtSecret = "your-secret-key-change-in-production"
|
||||
}
|
||||
auth.InitJWTSecret(jwtSecret)
|
||||
|
||||
// Initialize database
|
||||
dbPath := os.Getenv("DB_PATH")
|
||||
if dbPath == "" {
|
||||
dbPath = "/data/reservations.db"
|
||||
}
|
||||
|
||||
database, err := db.New(dbPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize database: %v", err)
|
||||
}
|
||||
defer database.Close()
|
||||
|
||||
// Create default admin user if none exists
|
||||
createDefaultUser(database)
|
||||
|
||||
// Start email processor
|
||||
emailProcessor := email.NewEmailProcessor(database)
|
||||
emailProcessor.Start()
|
||||
defer emailProcessor.Stop()
|
||||
|
||||
// Setup API handlers
|
||||
handler := api.NewHandler(database)
|
||||
mux := http.NewServeMux()
|
||||
handler.RegisterRoutes(mux)
|
||||
|
||||
// Static files
|
||||
fs := http.FileServer(http.Dir("/app/frontend"))
|
||||
mux.Handle("/", fs)
|
||||
|
||||
// Start server
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "8080"
|
||||
}
|
||||
|
||||
log.Printf("Server starting on port %s", port)
|
||||
if err := http.ListenAndServe(":"+port, corsMiddleware(mux)); err != nil {
|
||||
log.Fatalf("Server failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func createDefaultUser(database *db.DB) {
|
||||
adminUser, _ := database.GetUserByUsername("admin")
|
||||
if adminUser == nil {
|
||||
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("admin"), bcrypt.DefaultCost)
|
||||
user := &db.models.User{
|
||||
Username: "admin",
|
||||
Password: string(hashedPassword),
|
||||
IsAdmin: true,
|
||||
}
|
||||
if err := database.CreateUser(user); err != nil {
|
||||
log.Printf("Failed to create default user: %v", err)
|
||||
} else {
|
||||
log.Println("Created default admin user (admin/admin)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func corsMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,773 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
var jwtSecret []byte
|
||||
|
||||
func InitJWTSecret(secret string) {
|
||||
jwtSecret = []byte(secret)
|
||||
}
|
||||
|
||||
type Claims struct {
|
||||
Username string `json:"username"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func GenerateToken(username string, isAdmin bool) (string, error) {
|
||||
claims := Claims{
|
||||
Username: username,
|
||||
IsAdmin: isAdmin,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(jwtSecret)
|
||||
}
|
||||
|
||||
func ValidateToken(tokenString string) (*Claims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, errors.New("unexpected signing method")
|
||||
}
|
||||
return jwtSecret, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
@@ -0,0 +1,616 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"reservation-system/backend/internal/models"
|
||||
"time"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
conn *sql.DB
|
||||
}
|
||||
|
||||
func New(dbPath string) (*DB, error) {
|
||||
conn, err := sql.Open("sqlite3", dbPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := conn.Ping(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db := &DB{conn: conn}
|
||||
if err := db.Migrate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func (db *DB) Migrate() error {
|
||||
schema := `
|
||||
CREATE TABLE IF NOT EXISTS rooms (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
capacity INTEGER NOT NULL DEFAULT 0,
|
||||
color TEXT DEFAULT '#3b82f6',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tables (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
room_id INTEGER NOT NULL,
|
||||
x INTEGER DEFAULT 0,
|
||||
y INTEGER DEFAULT 0,
|
||||
width INTEGER DEFAULT 100,
|
||||
height INTEGER DEFAULT 100,
|
||||
max_guests INTEGER NOT NULL,
|
||||
shape TEXT DEFAULT 'rect',
|
||||
status TEXT DEFAULT 'active',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS reservations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
table_id INTEGER NOT NULL,
|
||||
date TEXT NOT NULL,
|
||||
time_from TEXT NOT NULL,
|
||||
time_to TEXT NOT NULL,
|
||||
guests INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
phone TEXT,
|
||||
email TEXT,
|
||||
source TEXT DEFAULT 'manual',
|
||||
notes TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (table_id) REFERENCES tables(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS room_bookings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
room_id INTEGER NOT NULL,
|
||||
date TEXT NOT NULL,
|
||||
time_from TEXT NOT NULL,
|
||||
time_to TEXT NOT NULL,
|
||||
guests INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
phone TEXT,
|
||||
email TEXT,
|
||||
event_type TEXT,
|
||||
notes TEXT,
|
||||
status TEXT DEFAULT 'confirmed',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS table_merges (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
parent_table_id INTEGER NOT NULL,
|
||||
child_table_id INTEGER NOT NULL,
|
||||
merged_name TEXT,
|
||||
active BOOLEAN DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (parent_table_id) REFERENCES tables(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (child_table_id) REFERENCES tables(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS email_config (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
host TEXT NOT NULL,
|
||||
port INTEGER DEFAULT 993,
|
||||
user TEXT NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
ssl BOOLEAN DEFAULT 1,
|
||||
folder TEXT DEFAULT 'INBOX'
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
is_admin BOOLEAN DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_reservations_date ON reservations(date);
|
||||
CREATE INDEX IF NOT EXISTS idx_reservations_table ON reservations(table_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tables_room ON tables(room_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_reservations_time ON reservations(time_from, time_to);
|
||||
CREATE INDEX IF NOT EXISTS idx_room_bookings_date ON room_bookings(date);
|
||||
CREATE INDEX IF NOT EXISTS idx_room_bookings_room ON room_bookings(room_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_room_bookings_time ON room_bookings(time_from, time_to);
|
||||
`
|
||||
|
||||
_, err := db.conn.Exec(schema)
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *DB) Close() error {
|
||||
return db.conn.Close()
|
||||
}
|
||||
|
||||
// Room Methods
|
||||
func (db *DB) CreateRoom(room *models.Room) error {
|
||||
result, err := db.conn.Exec(
|
||||
"INSERT INTO rooms (name, capacity, color) VALUES (?, ?, ?)",
|
||||
room.Name, room.Capacity, room.Color,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := result.LastInsertId()
|
||||
room.ID = int(id)
|
||||
room.CreatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) GetRooms() ([]models.Room, error) {
|
||||
rows, err := db.conn.Query("SELECT id, name, capacity, color, created_at FROM rooms ORDER BY name")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var rooms []models.Room
|
||||
for rows.Next() {
|
||||
var r models.Room
|
||||
if err := rows.Scan(&r.ID, &r.Name, &r.Capacity, &r.Color, &r.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rooms = append(rooms, r)
|
||||
}
|
||||
return rooms, nil
|
||||
}
|
||||
|
||||
func (db *DB) GetRoom(id int) (*models.Room, error) {
|
||||
var r models.Room
|
||||
err := db.conn.QueryRow(
|
||||
"SELECT id, name, capacity, color, created_at FROM rooms WHERE id = ?",
|
||||
id,
|
||||
).Scan(&r.ID, &r.Name, &r.Capacity, &r.Color, &r.CreatedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return &r, err
|
||||
}
|
||||
|
||||
func (db *DB) UpdateRoom(room *models.Room) error {
|
||||
_, err := db.conn.Exec(
|
||||
"UPDATE rooms SET name = ?, capacity = ?, color = ? WHERE id = ?",
|
||||
room.Name, room.Capacity, room.Color, room.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *DB) DeleteRoom(id int) error {
|
||||
_, err := db.conn.Exec("DELETE FROM rooms WHERE id = ?", id)
|
||||
return err
|
||||
}
|
||||
|
||||
// Table Methods
|
||||
func (db *DB) CreateTable(table *models.Table) error {
|
||||
result, err := db.conn.Exec(
|
||||
"INSERT INTO tables (room_id, x, y, width, height, max_guests, shape, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
table.RoomID, table.X, table.Y, table.Width, table.Height, table.MaxGuests, table.Shape, table.Status,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := result.LastInsertId()
|
||||
table.ID = int(id)
|
||||
table.CreatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) GetTablesByRoom(roomID int) ([]models.Table, error) {
|
||||
rows, err := db.conn.Query(`
|
||||
SELECT t.id, t.room_id, t.x, t.y, t.width, t.height, t.max_guests, t.shape, t.status, t.created_at, r.name as room_name
|
||||
FROM tables t
|
||||
JOIN rooms r ON t.room_id = r.id
|
||||
WHERE t.room_id = ? AND t.status = 'active'
|
||||
ORDER BY t.id`, roomID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var tables []models.Table
|
||||
for rows.Next() {
|
||||
var t models.Table
|
||||
if err := rows.Scan(&t.ID, &t.RoomID, &t.X, &t.Y, &t.Width, &t.Height, &t.MaxGuests, &t.Shape, &t.Status, &t.CreatedAt, &t.RoomName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tables = append(tables, t)
|
||||
}
|
||||
return tables, nil
|
||||
}
|
||||
|
||||
func (db *DB) GetAllTables() ([]models.Table, error) {
|
||||
rows, err := db.conn.Query(`
|
||||
SELECT t.id, t.room_id, t.x, t.y, t.width, t.height, t.max_guests, t.shape, t.status, t.created_at, r.name as room_name
|
||||
FROM tables t
|
||||
JOIN rooms r ON t.room_id = r.id
|
||||
WHERE t.status = 'active'
|
||||
ORDER BY t.room_id, t.id`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var tables []models.Table
|
||||
for rows.Next() {
|
||||
var t models.Table
|
||||
if err := rows.Scan(&t.ID, &t.RoomID, &t.X, &t.Y, &t.Width, &t.Height, &t.MaxGuests, &t.Shape, &t.Status, &t.CreatedAt, &t.RoomName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tables = append(tables, t)
|
||||
}
|
||||
return tables, nil
|
||||
}
|
||||
|
||||
func (db *DB) GetTable(id int) (*models.Table, error) {
|
||||
var t models.Table
|
||||
err := db.conn.QueryRow(`
|
||||
SELECT t.id, t.room_id, t.x, t.y, t.width, t.height, t.max_guests, t.shape, t.status, t.created_at, r.name as room_name
|
||||
FROM tables t
|
||||
JOIN rooms r ON t.room_id = r.id
|
||||
WHERE t.id = ?`, id).Scan(&t.ID, &t.RoomID, &t.X, &t.Y, &t.Width, &t.Height, &t.MaxGuests, &t.Shape, &t.Status, &t.CreatedAt, &t.RoomName)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return &t, err
|
||||
}
|
||||
|
||||
func (db *DB) UpdateTable(table *models.Table) error {
|
||||
_, err := db.conn.Exec(
|
||||
"UPDATE tables SET room_id = ?, x = ?, y = ?, width = ?, height = ?, max_guests = ?, shape = ? WHERE id = ?",
|
||||
table.RoomID, table.X, table.Y, table.Width, table.Height, table.MaxGuests, table.Shape, table.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *DB) DeleteTable(id int) error {
|
||||
_, err := db.conn.Exec("UPDATE tables SET status = 'deleted' WHERE id = ?", id)
|
||||
return err
|
||||
}
|
||||
|
||||
// Reservation Methods
|
||||
func (db *DB) CreateReservation(r *models.Reservation) error {
|
||||
result, err := db.conn.Exec(
|
||||
"INSERT INTO reservations (table_id, date, time_from, time_to, guests, name, phone, email, source, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
r.TableID, r.Date, r.TimeFrom, r.TimeTo, r.Guests, r.Name, r.Phone, r.Email, r.Source, r.Notes,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := result.LastInsertId()
|
||||
r.ID = int(id)
|
||||
r.CreatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) GetReservationsByDate(date string) ([]models.Reservation, error) {
|
||||
rows, err := db.conn.Query(`
|
||||
SELECT r.id, r.table_id, r.date, r.time_from, r.time_to, r.guests, r.name, r.phone, r.email, r.source, r.notes, r.created_at, r.table_id as table_name
|
||||
FROM reservations r
|
||||
WHERE r.date = ?
|
||||
ORDER BY r.time_from`, date)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var reservations []models.Reservation
|
||||
for rows.Next() {
|
||||
var res models.Reservation
|
||||
if err := rows.Scan(&res.ID, &res.TableID, &res.Date, &res.TimeFrom, &res.TimeTo, &res.Guests, &res.Name, &res.Phone, &res.Email, &res.Source, &res.Notes, &res.CreatedAt, &res.TableName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reservations = append(reservations, res)
|
||||
}
|
||||
return reservations, nil
|
||||
}
|
||||
|
||||
func (db *DB) GetReservationsByTable(tableID int, date string) ([]models.Reservation, error) {
|
||||
rows, err := db.conn.Query(`
|
||||
SELECT id, table_id, date, time_from, time_to, guests, name, phone, email, source, notes, created_at
|
||||
FROM reservations
|
||||
WHERE table_id = ? AND date = ?
|
||||
ORDER BY time_from`, tableID, date)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var reservations []models.Reservation
|
||||
for rows.Next() {
|
||||
var res models.Reservation
|
||||
if err := rows.Scan(&res.ID, &res.TableID, &res.Date, &res.TimeFrom, &res.TimeTo, &res.Guests, &res.Name, &res.Phone, &res.Email, &res.Source, &res.Notes, &res.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reservations = append(reservations, res)
|
||||
}
|
||||
return reservations, nil
|
||||
}
|
||||
|
||||
func (db *DB) GetReservation(id int) (*models.Reservation, error) {
|
||||
var r models.Reservation
|
||||
err := db.conn.QueryRow(`
|
||||
SELECT id, table_id, date, time_from, time_to, guests, name, phone, email, source, notes, created_at
|
||||
FROM reservations WHERE id = ?`, id).Scan(&r.ID, &r.TableID, &r.Date, &r.TimeFrom, &r.TimeTo, &r.Guests, &r.Name, &r.Phone, &r.Email, &r.Source, &r.Notes, &r.CreatedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return &r, err
|
||||
}
|
||||
|
||||
func (db *DB) UpdateReservation(r *models.Reservation) error {
|
||||
_, err := db.conn.Exec(
|
||||
"UPDATE reservations SET table_id = ?, date = ?, time_from = ?, time_to = ?, guests = ?, name = ?, phone = ?, email = ?, notes = ? WHERE id = ?",
|
||||
r.TableID, r.Date, r.TimeFrom, r.TimeTo, r.Guests, r.Name, r.Phone, r.Email, r.Notes, r.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *DB) DeleteReservation(id int) error {
|
||||
_, err := db.conn.Exec("DELETE FROM reservations WHERE id = ?", id)
|
||||
return err
|
||||
}
|
||||
|
||||
// Room Booking Methods (NEW)
|
||||
func (db *DB) CreateRoomBooking(rb *models.RoomBooking) error {
|
||||
result, err := db.conn.Exec(
|
||||
"INSERT INTO room_bookings (room_id, date, time_from, time_to, guests, name, phone, email, event_type, notes, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
rb.RoomID, rb.Date, rb.TimeFrom, rb.TimeTo, rb.Guests, rb.Name, rb.Phone, rb.Email, rb.EventType, rb.Notes, rb.Status,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := result.LastInsertId()
|
||||
rb.ID = int(id)
|
||||
rb.CreatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) GetRoomBookingsByRoom(roomID int) ([]models.RoomBooking, error) {
|
||||
rows, err := db.conn.Query(`
|
||||
SELECT rb.id, rb.room_id, rb.date, rb.time_from, rb.time_to, rb.guests, rb.name, rb.phone, rb.email, rb.event_type, rb.notes, rb.status, rb.created_at, r.name as room_name
|
||||
FROM room_bookings rb
|
||||
JOIN rooms r ON rb.room_id = r.id
|
||||
WHERE rb.room_id = ?
|
||||
ORDER BY rb.date DESC, rb.time_from`, roomID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var bookings []models.RoomBooking
|
||||
for rows.Next() {
|
||||
var b models.RoomBooking
|
||||
if err := rows.Scan(&b.ID, &b.RoomID, &b.Date, &b.TimeFrom, &b.TimeTo, &b.Guests, &b.Name, &b.Phone, &b.Email, &b.EventType, &b.Notes, &b.Status, &b.CreatedAt, &b.RoomName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bookings = append(bookings, b)
|
||||
}
|
||||
return bookings, nil
|
||||
}
|
||||
|
||||
func (db *DB) GetRoomBookingsByDate(date string) ([]models.RoomBooking, error) {
|
||||
rows, err := db.conn.Query(`
|
||||
SELECT rb.id, rb.room_id, rb.date, rb.time_from, rb.time_to, rb.guests, rb.name, rb.phone, rb.email, rb.event_type, rb.notes, rb.status, rb.created_at, r.name as room_name
|
||||
FROM room_bookings rb
|
||||
JOIN rooms r ON rb.room_id = r.id
|
||||
WHERE rb.date = ?
|
||||
ORDER BY rb.time_from`, date)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var bookings []models.RoomBooking
|
||||
for rows.Next() {
|
||||
var b models.RoomBooking
|
||||
if err := rows.Scan(&b.ID, &b.RoomID, &b.Date, &b.TimeFrom, &b.TimeTo, &b.Guests, &b.Name, &b.Phone, &b.Email, &b.EventType, &b.Notes, &b.Status, &b.CreatedAt, &b.RoomName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bookings = append(bookings, b)
|
||||
}
|
||||
return bookings, nil
|
||||
}
|
||||
|
||||
func (db *DB) GetRoomBooking(id int) (*models.RoomBooking, error) {
|
||||
var rb models.RoomBooking
|
||||
err := db.conn.QueryRow(`
|
||||
SELECT rb.id, rb.room_id, rb.date, rb.time_from, rb.time_to, rb.guests, rb.name, rb.phone, rb.email, rb.event_type, rb.notes, rb.status, rb.created_at, r.name as room_name
|
||||
FROM room_bookings rb
|
||||
JOIN rooms r ON rb.room_id = r.id
|
||||
WHERE rb.id = ?`, id).Scan(&rb.ID, &rb.RoomID, &rb.Date, &rb.TimeFrom, &rb.TimeTo, &rb.Guests, &rb.Name, &rb.Phone, &rb.Email, &rb.EventType, &rb.Notes, &rb.Status, &rb.CreatedAt, &rb.RoomName)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return &rb, err
|
||||
}
|
||||
|
||||
func (db *DB) UpdateRoomBooking(rb *models.RoomBooking) error {
|
||||
_, err := db.conn.Exec(
|
||||
"UPDATE room_bookings SET room_id = ?, date = ?, time_from = ?, time_to = ?, guests = ?, name = ?, phone = ?, email = ?, event_type = ?, notes = ?, status = ? WHERE id = ?",
|
||||
rb.RoomID, rb.Date, rb.TimeFrom, rb.TimeTo, rb.Guests, rb.Name, rb.Phone, rb.Email, rb.EventType, rb.Notes, rb.Status, rb.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *DB) DeleteRoomBooking(id int) error {
|
||||
_, err := db.conn.Exec("DELETE FROM room_bookings WHERE id = ?", id)
|
||||
return err
|
||||
}
|
||||
|
||||
// Availability Checks
|
||||
func (db *DB) CheckAvailability(tableID int, date, timeFrom, timeTo string) (bool, error) {
|
||||
var count int
|
||||
err := db.conn.QueryRow(`
|
||||
SELECT COUNT(*) FROM reservations
|
||||
WHERE table_id = ? AND date = ?
|
||||
AND (
|
||||
(time_from < ? AND time_to > ?) OR
|
||||
(time_from < ? AND time_to > ?) OR
|
||||
(time_from >= ? AND time_to <= ?)
|
||||
)`, tableID, date, timeTo, timeFrom, timeTo, timeFrom, timeFrom, timeTo).Scan(&count)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count == 0, nil
|
||||
}
|
||||
|
||||
func (db *DB) CheckRoomAvailability(roomID int, date, timeFrom, timeTo string) (bool, error) {
|
||||
var count int
|
||||
err := db.conn.QueryRow(`
|
||||
SELECT COUNT(*) FROM room_bookings
|
||||
WHERE room_id = ? AND date = ? AND status = 'confirmed'
|
||||
AND (
|
||||
(time_from < ? AND time_to > ?) OR
|
||||
(time_from < ? AND time_to > ?) OR
|
||||
(time_from >= ? AND time_to <= ?)
|
||||
)`, roomID, date, timeTo, timeFrom, timeTo, timeFrom, timeFrom, timeTo).Scan(&count)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count == 0, nil
|
||||
}
|
||||
|
||||
func (db *DB) GetTableMerges(parentTableID int) ([]models.TableMerge, error) {
|
||||
rows, err := db.conn.Query(`
|
||||
SELECT id, parent_table_id, child_table_id, merged_name, active, created_at
|
||||
FROM table_merges
|
||||
WHERE parent_table_id = ? AND active = 1`, parentTableID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var merges []models.TableMerge
|
||||
for rows.Next() {
|
||||
var m models.TableMerge
|
||||
if err := rows.Scan(&m.ID, &m.ParentTableID, &m.ChildTableID, &m.MergedName, &m.Active, &m.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
merges = append(merges, m)
|
||||
}
|
||||
return merges, nil
|
||||
}
|
||||
|
||||
func (db *DB) UnmergeTables(mergeID int) error {
|
||||
var childID int
|
||||
err := db.conn.QueryRow("SELECT child_table_id FROM table_merges WHERE id = ?", mergeID).Scan(&childID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.conn.Exec("UPDATE table_merges SET active = 0 WHERE id = ?", mergeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.conn.Exec("UPDATE tables SET status = 'active' WHERE id = ?", childID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *DB) GetEmailConfig() (*models.EmailConfig, error) {
|
||||
var cfg models.EmailConfig
|
||||
err := db.conn.QueryRow("SELECT id, host, port, user, password, ssl, folder FROM email_config LIMIT 1").Scan(
|
||||
&cfg.ID, &cfg.Host, &cfg.Port, &cfg.Username, &cfg.Password, &cfg.SSL, &cfg.Folder,
|
||||
)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return &cfg, err
|
||||
}
|
||||
|
||||
func (db *DB) SaveEmailConfig(cfg *models.EmailConfig) error {
|
||||
if cfg.ID > 0 {
|
||||
_, err := db.conn.Exec(
|
||||
"UPDATE email_config SET host = ?, port = ?, user = ?, password = ?, ssl = ?, folder = ? WHERE id = ?",
|
||||
cfg.Host, cfg.Port, &cfg.Username, cfg.Password, cfg.SSL, cfg.Folder, cfg.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
result, err := db.conn.Exec(
|
||||
"INSERT INTO email_config (host, port, user, password, ssl, folder) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
cfg.Host, cfg.Port, cfg.Username, cfg.Password, cfg.SSL, cfg.Folder,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := result.LastInsertId()
|
||||
cfg.ID = int(id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) GetUserByUsername(username string) (*models.User, error) {
|
||||
var u models.User
|
||||
err := db.conn.QueryRow("SELECT id, username, password, is_admin FROM users WHERE username = ?", username).Scan(
|
||||
&u.ID, &u.Username, &u.Password, &u.IsAdmin,
|
||||
)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return &u, err
|
||||
}
|
||||
|
||||
func (db *DB) CreateUser(user *models.User) error {
|
||||
result, err := db.conn.Exec(
|
||||
"INSERT INTO users (username, password, is_admin) VALUES (?, ?, ?)",
|
||||
user.Username, user.Password, user.IsAdmin,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := result.LastInsertId()
|
||||
user.ID = int(id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) GetMergedTablesFor(parentTableID int) ([]int, error) {
|
||||
rows, err := db.conn.Query("SELECT child_table_id FROM table_merges WHERE parent_table_id = ? AND active = 1", parentTableID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var ids []int
|
||||
for rows.Next() {
|
||||
var id int
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (db *DB) GetTotalGuestsForDate(date string) (int, error) {
|
||||
var total int
|
||||
err := db.conn.QueryRow("SELECT COALESCE(SUM(guests), 0) FROM reservations WHERE date = ?", date).Scan(&total)
|
||||
return total, err
|
||||
}
|
||||
|
||||
func (db *DB) CreateTableMerge(merge *models.TableMerge) error {
|
||||
result, err := db.conn.Exec(
|
||||
"INSERT INTO table_merges (parent_table_id, child_table_id, merged_name, active) VALUES (?, ?, ?, ?)",
|
||||
merge.ParentTableID, merge.ChildTableID, merge.MergedName, merge.Active,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := result.LastInsertId()
|
||||
merge.ID = int(id)
|
||||
merge.CreatedAt = time.Now()
|
||||
|
||||
// Mark child table as merged
|
||||
_, err = db.conn.Exec("UPDATE tables SET status = 'merged' WHERE id = ?", merge.ChildTableID)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,616 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"reservation-system/backend/internal/models"
|
||||
"time"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
conn *sql.DB
|
||||
}
|
||||
|
||||
func New(dbPath string) (*DB, error) {
|
||||
conn, err := sql.Open("sqlite3", dbPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := conn.Ping(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db := &DB{conn: conn}
|
||||
if err := db.Migrate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func (db *DB) Migrate() error {
|
||||
schema := `
|
||||
CREATE TABLE IF NOT EXISTS rooms (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
capacity INTEGER NOT NULL DEFAULT 0,
|
||||
color TEXT DEFAULT '#3b82f6',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tables (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
room_id INTEGER NOT NULL,
|
||||
x INTEGER DEFAULT 0,
|
||||
y INTEGER DEFAULT 0,
|
||||
width INTEGER DEFAULT 100,
|
||||
height INTEGER DEFAULT 100,
|
||||
max_guests INTEGER NOT NULL,
|
||||
shape TEXT DEFAULT 'rect',
|
||||
status TEXT DEFAULT 'active',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS reservations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
table_id INTEGER NOT NULL,
|
||||
date TEXT NOT NULL,
|
||||
time_from TEXT NOT NULL,
|
||||
time_to TEXT NOT NULL,
|
||||
guests INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
phone TEXT,
|
||||
email TEXT,
|
||||
source TEXT DEFAULT 'manual',
|
||||
notes TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (table_id) REFERENCES tables(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS room_bookings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
room_id INTEGER NOT NULL,
|
||||
date TEXT NOT NULL,
|
||||
time_from TEXT NOT NULL,
|
||||
time_to TEXT NOT NULL,
|
||||
guests INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
phone TEXT,
|
||||
email TEXT,
|
||||
event_type TEXT,
|
||||
notes TEXT,
|
||||
status TEXT DEFAULT 'confirmed',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS table_merges (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
parent_table_id INTEGER NOT NULL,
|
||||
child_table_id INTEGER NOT NULL,
|
||||
merged_name TEXT,
|
||||
active BOOLEAN DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (parent_table_id) REFERENCES tables(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (child_table_id) REFERENCES tables(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS email_config (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
host TEXT NOT NULL,
|
||||
port INTEGER DEFAULT 993,
|
||||
user TEXT NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
ssl BOOLEAN DEFAULT 1,
|
||||
folder TEXT DEFAULT 'INBOX'
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
is_admin BOOLEAN DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_reservations_date ON reservations(date);
|
||||
CREATE INDEX IF NOT EXISTS idx_reservations_table ON reservations(table_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tables_room ON tables(room_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_reservations_time ON reservations(time_from, time_to);
|
||||
CREATE INDEX IF NOT EXISTS idx_room_bookings_date ON room_bookings(date);
|
||||
CREATE INDEX IF NOT EXISTS idx_room_bookings_room ON room_bookings(room_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_room_bookings_time ON room_bookings(time_from, time_to);
|
||||
`
|
||||
|
||||
_, err := db.conn.Exec(schema)
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *DB) Close() error {
|
||||
return db.conn.Close()
|
||||
}
|
||||
|
||||
// Room Methods
|
||||
func (db *DB) CreateRoom(room *models.Room) error {
|
||||
result, err := db.conn.Exec(
|
||||
"INSERT INTO rooms (name, capacity, color) VALUES (?, ?, ?)",
|
||||
room.Name, room.Capacity, room.Color,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := result.LastInsertId()
|
||||
room.ID = int(id)
|
||||
room.CreatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) GetRooms() ([]models.Room, error) {
|
||||
rows, err := db.conn.Query("SELECT id, name, capacity, color, created_at FROM rooms ORDER BY name")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var rooms []models.Room
|
||||
for rows.Next() {
|
||||
var r models.Room
|
||||
if err := rows.Scan(&r.ID, &r.Name, &r.Capacity, &r.Color, &r.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rooms = append(rooms, r)
|
||||
}
|
||||
return rooms, nil
|
||||
}
|
||||
|
||||
func (db *DB) GetRoom(id int) (*models.Room, error) {
|
||||
var r models.Room
|
||||
err := db.conn.QueryRow(
|
||||
"SELECT id, name, capacity, color, created_at FROM rooms WHERE id = ?",
|
||||
id,
|
||||
).Scan(&r.ID, &r.Name, &r.Capacity, &r.Color, &r.CreatedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return &r, err
|
||||
}
|
||||
|
||||
func (db *DB) UpdateRoom(room *models.Room) error {
|
||||
_, err := db.conn.Exec(
|
||||
"UPDATE rooms SET name = ?, capacity = ?, color = ? WHERE id = ?",
|
||||
room.Name, room.Capacity, room.Color, room.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *DB) DeleteRoom(id int) error {
|
||||
_, err := db.conn.Exec("DELETE FROM rooms WHERE id = ?", id)
|
||||
return err
|
||||
}
|
||||
|
||||
// Table Methods
|
||||
func (db *DB) CreateTable(table *models.Table) error {
|
||||
result, err := db.conn.Exec(
|
||||
"INSERT INTO tables (room_id, x, y, width, height, max_guests, shape, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
table.RoomID, table.X, table.Y, table.Width, table.Height, table.MaxGuests, table.Shape, table.Status,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := result.LastInsertId()
|
||||
table.ID = int(id)
|
||||
table.CreatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) GetTablesByRoom(roomID int) ([]models.Table, error) {
|
||||
rows, err := db.conn.Query(`
|
||||
SELECT t.id, t.room_id, t.x, t.y, t.width, t.height, t.max_guests, t.shape, t.status, t.created_at, r.name as room_name
|
||||
FROM tables t
|
||||
JOIN rooms r ON t.room_id = r.id
|
||||
WHERE t.room_id = ? AND t.status = 'active'
|
||||
ORDER BY t.id`, roomID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var tables []models.Table
|
||||
for rows.Next() {
|
||||
var t models.Table
|
||||
if err := rows.Scan(&t.ID, &t.RoomID, &t.X, &t.Y, &t.Width, &t.Height, &t.MaxGuests, &t.Shape, &t.Status, &t.CreatedAt, &t.RoomName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tables = append(tables, t)
|
||||
}
|
||||
return tables, nil
|
||||
}
|
||||
|
||||
func (db *DB) GetAllTables() ([]models.Table, error) {
|
||||
rows, err := db.conn.Query(`
|
||||
SELECT t.id, t.room_id, t.x, t.y, t.width, t.height, t.max_guests, t.shape, t.status, t.created_at, r.name as room_name
|
||||
FROM tables t
|
||||
JOIN rooms r ON t.room_id = r.id
|
||||
WHERE t.status = 'active'
|
||||
ORDER BY t.room_id, t.id`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var tables []models.Table
|
||||
for rows.Next() {
|
||||
var t models.Table
|
||||
if err := rows.Scan(&t.ID, &t.RoomID, &t.X, &t.Y, &t.Width, &t.Height, &t.MaxGuests, &t.Shape, &t.Status, &t.CreatedAt, &t.RoomName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tables = append(tables, t)
|
||||
}
|
||||
return tables, nil
|
||||
}
|
||||
|
||||
func (db *DB) GetTable(id int) (*models.Table, error) {
|
||||
var t models.Table
|
||||
err := db.conn.QueryRow(`
|
||||
SELECT t.id, t.room_id, t.x, t.y, t.width, t.height, t.max_guests, t.shape, t.status, t.created_at, r.name as room_name
|
||||
FROM tables t
|
||||
JOIN rooms r ON t.room_id = r.id
|
||||
WHERE t.id = ?`, id).Scan(&t.ID, &t.RoomID, &t.X, &t.Y, &t.Width, &t.Height, &t.MaxGuests, &t.Shape, &t.Status, &t.CreatedAt, &t.RoomName)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return &t, err
|
||||
}
|
||||
|
||||
func (db *DB) UpdateTable(table *models.Table) error {
|
||||
_, err := db.conn.Exec(
|
||||
"UPDATE tables SET room_id = ?, x = ?, y = ?, width = ?, height = ?, max_guests = ?, shape = ? WHERE id = ?",
|
||||
table.RoomID, table.X, table.Y, table.Width, table.Height, table.MaxGuests, table.Shape, table.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *DB) DeleteTable(id int) error {
|
||||
_, err := db.conn.Exec("UPDATE tables SET status = 'deleted' WHERE id = ?", id)
|
||||
return err
|
||||
}
|
||||
|
||||
// Reservation Methods
|
||||
func (db *DB) CreateReservation(r *models.Reservation) error {
|
||||
result, err := db.conn.Exec(
|
||||
"INSERT INTO reservations (table_id, date, time_from, time_to, guests, name, phone, email, source, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
r.TableID, r.Date, r.TimeFrom, r.TimeTo, r.Guests, r.Name, r.Phone, r.Email, r.Source, r.Notes,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := result.LastInsertId()
|
||||
r.ID = int(id)
|
||||
r.CreatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) GetReservationsByDate(date string) ([]models.Reservation, error) {
|
||||
rows, err := db.conn.Query(`
|
||||
SELECT r.id, r.table_id, r.date, r.time_from, r.time_to, r.guests, r.name, r.phone, r.email, r.source, r.notes, r.created_at, r.table_id as table_name
|
||||
FROM reservations r
|
||||
WHERE r.date = ?
|
||||
ORDER BY r.time_from`, date)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var reservations []models.Reservation
|
||||
for rows.Next() {
|
||||
var res models.Reservation
|
||||
if err := rows.Scan(&res.ID, &res.TableID, &res.Date, &res.TimeFrom, &res.TimeTo, &res.Guests, &res.Name, &res.Phone, &res.Email, &res.Source, &res.Notes, &res.CreatedAt, &res.TableName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reservations = append(reservations, res)
|
||||
}
|
||||
return reservations, nil
|
||||
}
|
||||
|
||||
func (db *DB) GetReservationsByTable(tableID int, date string) ([]models.Reservation, error) {
|
||||
rows, err := db.conn.Query(`
|
||||
SELECT id, table_id, date, time_from, time_to, guests, name, phone, email, source, notes, created_at
|
||||
FROM reservations
|
||||
WHERE table_id = ? AND date = ?
|
||||
ORDER BY time_from`, tableID, date)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var reservations []models.Reservation
|
||||
for rows.Next() {
|
||||
var res models.Reservation
|
||||
if err := rows.Scan(&res.ID, &res.TableID, &res.Date, &res.TimeFrom, &res.TimeTo, &res.Guests, &res.Name, &res.Phone, &res.Email, &res.Source, &res.Notes, &res.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reservations = append(reservations, res)
|
||||
}
|
||||
return reservations, nil
|
||||
}
|
||||
|
||||
func (db *DB) GetReservation(id int) (*models.Reservation, error) {
|
||||
var r models.Reservation
|
||||
err := db.conn.QueryRow(`
|
||||
SELECT id, table_id, date, time_from, time_to, guests, name, phone, email, source, notes, created_at
|
||||
FROM reservations WHERE id = ?`, id).Scan(&r.ID, &r.TableID, &r.Date, &r.TimeFrom, &r.TimeTo, &r.Guests, &r.Name, &r.Phone, &r.Email, &r.Source, &r.Notes, &r.CreatedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return &r, err
|
||||
}
|
||||
|
||||
func (db *DB) UpdateReservation(r *models.Reservation) error {
|
||||
_, err := db.conn.Exec(
|
||||
"UPDATE reservations SET table_id = ?, date = ?, time_from = ?, time_to = ?, guests = ?, name = ?, phone = ?, email = ?, notes = ? WHERE id = ?",
|
||||
r.TableID, r.Date, r.TimeFrom, r.TimeTo, r.Guests, r.Name, r.Phone, r.Email, r.Notes, r.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *DB) DeleteReservation(id int) error {
|
||||
_, err := db.conn.Exec("DELETE FROM reservations WHERE id = ?", id)
|
||||
return err
|
||||
}
|
||||
|
||||
// Room Booking Methods (NEW)
|
||||
func (db *DB) CreateRoomBooking(rb *models.RoomBooking) error {
|
||||
result, err := db.conn.Exec(
|
||||
"INSERT INTO room_bookings (room_id, date, time_from, time_to, guests, name, phone, email, event_type, notes, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
rb.RoomID, rb.Date, rb.TimeFrom, rb.TimeTo, rb.Guests, rb.Name, rb.Phone, rb.Email, rb.EventType, rb.Notes, rb.Status,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := result.LastInsertId()
|
||||
rb.ID = int(id)
|
||||
rb.CreatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) GetRoomBookingsByRoom(roomID int) ([]models.RoomBooking, error) {
|
||||
rows, err := db.conn.Query(`
|
||||
SELECT rb.id, rb.room_id, rb.date, rb.time_from, rb.time_to, rb.guests, rb.name, rb.phone, rb.email, rb.event_type, rb.notes, rb.status, rb.created_at, r.name as room_name
|
||||
FROM room_bookings rb
|
||||
JOIN rooms r ON rb.room_id = r.id
|
||||
WHERE rb.room_id = ?
|
||||
ORDER BY rb.date DESC, rb.time_from`, roomID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var bookings []models.RoomBooking
|
||||
for rows.Next() {
|
||||
var b models.RoomBooking
|
||||
if err := rows.Scan(&b.ID, &b.RoomID, &b.Date, &b.TimeFrom, &b.TimeTo, &b.Guests, &b.Name, &b.Phone, &b.Email, &b.EventType, &b.Notes, &b.Status, &b.CreatedAt, &b.RoomName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bookings = append(bookings, b)
|
||||
}
|
||||
return bookings, nil
|
||||
}
|
||||
|
||||
func (db *DB) GetRoomBookingsByDate(date string) ([]models.RoomBooking, error) {
|
||||
rows, err := db.conn.Query(`
|
||||
SELECT rb.id, rb.room_id, rb.date, rb.time_from, rb.time_to, rb.guests, rb.name, rb.phone, rb.email, rb.event_type, rb.notes, rb.status, rb.created_at, r.name as room_name
|
||||
FROM room_bookings rb
|
||||
JOIN rooms r ON rb.room_id = r.id
|
||||
WHERE rb.date = ?
|
||||
ORDER BY rb.time_from`, date)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var bookings []models.RoomBooking
|
||||
for rows.Next() {
|
||||
var b models.RoomBooking
|
||||
if err := rows.Scan(&b.ID, &b.RoomID, &b.Date, &b.TimeFrom, &b.TimeTo, &b.Guests, &b.Name, &b.Phone, &b.Email, &b.EventType, &b.Notes, &b.Status, &b.CreatedAt, &b.RoomName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bookings = append(bookings, b)
|
||||
}
|
||||
return bookings, nil
|
||||
}
|
||||
|
||||
func (db *DB) GetRoomBooking(id int) (*models.RoomBooking, error) {
|
||||
var rb models.RoomBooking
|
||||
err := db.conn.QueryRow(`
|
||||
SELECT rb.id, rb.room_id, rb.date, rb.time_from, rb.time_to, rb.guests, rb.name, rb.phone, rb.email, rb.event_type, rb.notes, rb.status, rb.created_at, r.name as room_name
|
||||
FROM room_bookings rb
|
||||
JOIN rooms r ON rb.room_id = r.id
|
||||
WHERE rb.id = ?`, id).Scan(&rb.ID, &rb.RoomID, &rb.Date, &rb.TimeFrom, &rb.TimeTo, &rb.Guests, &rb.Name, &rb.Phone, &rb.Email, &rb.EventType, &rb.Notes, &rb.Status, &rb.CreatedAt, &rb.RoomName)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return &rb, err
|
||||
}
|
||||
|
||||
func (db *DB) UpdateRoomBooking(rb *models.RoomBooking) error {
|
||||
_, err := db.conn.Exec(
|
||||
"UPDATE room_bookings SET room_id = ?, date = ?, time_from = ?, time_to = ?, guests = ?, name = ?, phone = ?, email = ?, event_type = ?, notes = ?, status = ? WHERE id = ?",
|
||||
rb.RoomID, rb.Date, rb.TimeFrom, rb.TimeTo, rb.Guests, rb.Name, rb.Phone, rb.Email, rb.EventType, rb.Notes, rb.Status, rb.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *DB) DeleteRoomBooking(id int) error {
|
||||
_, err := db.conn.Exec("DELETE FROM room_bookings WHERE id = ?", id)
|
||||
return err
|
||||
}
|
||||
|
||||
// Availability Checks
|
||||
func (db *DB) CheckAvailability(tableID int, date, timeFrom, timeTo string) (bool, error) {
|
||||
var count int
|
||||
err := db.conn.QueryRow(`
|
||||
SELECT COUNT(*) FROM reservations
|
||||
WHERE table_id = ? AND date = ?
|
||||
AND (
|
||||
(time_from < ? AND time_to > ?) OR
|
||||
(time_from < ? AND time_to > ?) OR
|
||||
(time_from >= ? AND time_to <= ?)
|
||||
)`, tableID, date, timeTo, timeFrom, timeTo, timeFrom, timeFrom, timeTo).Scan(&count)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count == 0, nil
|
||||
}
|
||||
|
||||
func (db *DB) CheckRoomAvailability(roomID int, date, timeFrom, timeTo string) (bool, error) {
|
||||
var count int
|
||||
err := db.conn.QueryRow(`
|
||||
SELECT COUNT(*) FROM room_bookings
|
||||
WHERE room_id = ? AND date = ? AND status = 'confirmed'
|
||||
AND (
|
||||
(time_from < ? AND time_to > ?) OR
|
||||
(time_from < ? AND time_to > ?) OR
|
||||
(time_from >= ? AND time_to <= ?)
|
||||
)`, roomID, date, timeTo, timeFrom, timeTo, timeFrom, timeFrom, timeTo).Scan(&count)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count == 0, nil
|
||||
}
|
||||
|
||||
func (db *DB) GetTableMerges(parentTableID int) ([]models.TableMerge, error) {
|
||||
rows, err := db.conn.Query(`
|
||||
SELECT id, parent_table_id, child_table_id, merged_name, active, created_at
|
||||
FROM table_merges
|
||||
WHERE parent_table_id = ? AND active = 1`, parentTableID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var merges []models.TableMerge
|
||||
for rows.Next() {
|
||||
var m models.TableMerge
|
||||
if err := rows.Scan(&m.ID, &m.ParentTableID, &m.ChildTableID, &m.MergedName, &m.Active, &m.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
merges = append(merges, m)
|
||||
}
|
||||
return merges, nil
|
||||
}
|
||||
|
||||
func (db *DB) UnmergeTables(mergeID int) error {
|
||||
var childID int
|
||||
err := db.conn.QueryRow("SELECT child_table_id FROM table_merges WHERE id = ?", mergeID).Scan(&childID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.conn.Exec("UPDATE table_merges SET active = 0 WHERE id = ?", mergeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.conn.Exec("UPDATE tables SET status = 'active' WHERE id = ?", childID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *DB) GetEmailConfig() (*models.EmailConfig, error) {
|
||||
var cfg models.EmailConfig
|
||||
err := db.conn.QueryRow("SELECT id, host, port, user, password, ssl, folder FROM email_config LIMIT 1").Scan(
|
||||
&cfg.ID, &cfg.Host, &cfg.Port, &cfg.Username, &cfg.Password, &cfg.SSL, &cfg.Folder,
|
||||
)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return &cfg, err
|
||||
}
|
||||
|
||||
func (db *DB) SaveEmailConfig(cfg *models.EmailConfig) error {
|
||||
if cfg.ID > 0 {
|
||||
_, err := db.conn.Exec(
|
||||
"UPDATE email_config SET host = ?, port = ?, user = ?, password = ?, ssl = ?, folder = ? WHERE id = ?",
|
||||
cfg.Host, cfg.Port, &cfg.Username, cfg.Password, cfg.SSL, cfg.Folder, cfg.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
result, err := db.conn.Exec(
|
||||
"INSERT INTO email_config (host, port, user, password, ssl, folder) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
cfg.Host, cfg.Port, cfg.Username, cfg.Password, cfg.SSL, cfg.Folder,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := result.LastInsertId()
|
||||
cfg.ID = int(id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) GetUserByUsername(username string) (*models.User, error) {
|
||||
var u models.User
|
||||
err := db.conn.QueryRow("SELECT id, username, password, is_admin FROM users WHERE username = ?", username).Scan(
|
||||
&u.ID, &u.Username, &u.Password, &u.IsAdmin,
|
||||
)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return &u, err
|
||||
}
|
||||
|
||||
func (db *DB) CreateUser(user *models.User) error {
|
||||
result, err := db.conn.Exec(
|
||||
"INSERT INTO users (username, password, is_admin) VALUES (?, ?, ?)",
|
||||
user.Username, user.Password, user.IsAdmin,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := result.LastInsertId()
|
||||
user.ID = int(id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) GetMergedTablesFor(parentTableID int) ([]int, error) {
|
||||
rows, err := db.conn.Query("SELECT child_table_id FROM table_merges WHERE parent_table_id = ? AND active = 1", parentTableID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var ids []int
|
||||
for rows.Next() {
|
||||
var id int
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (db *DB) GetTotalGuestsForDate(date string) (int, error) {
|
||||
var total int
|
||||
err := db.conn.QueryRow("SELECT COALESCE(SUM(guests), 0) FROM reservations WHERE date = ?", date).Scan(&total)
|
||||
return total, err
|
||||
}
|
||||
|
||||
func (db *DB) CreateTableMerge(merge *models.TableMerge) error {
|
||||
result, err := db.conn.Exec(
|
||||
"INSERT INTO table_merges (parent_table_id, child_table_id, merged_name, active) VALUES (?, ?, ?, ?)",
|
||||
merge.ParentTableID, merge.ChildTableID, merge.MergedName, merge.Active,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := result.LastInsertId()
|
||||
merge.ID = int(id)
|
||||
merge.CreatedAt = time.Now()
|
||||
|
||||
// Mark child table as merged
|
||||
_, err = db.conn.Exec("UPDATE tables SET status = 'merged' WHERE id = ?", merge.ChildTableID)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"reservation-system/backend/internal/db"
|
||||
"reservation-system/backend/internal/models"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/client"
|
||||
)
|
||||
|
||||
type EmailProcessor struct {
|
||||
db *db.DB
|
||||
running bool
|
||||
stop chan bool
|
||||
}
|
||||
|
||||
func NewEmailProcessor(database *db.DB) *EmailProcessor {
|
||||
return &EmailProcessor{
|
||||
db: database,
|
||||
stop: make(chan bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (ep *EmailProcessor) Start() {
|
||||
if ep.running {
|
||||
return
|
||||
}
|
||||
ep.running = true
|
||||
go ep.pollLoop()
|
||||
}
|
||||
|
||||
func (ep *EmailProcessor) Stop() {
|
||||
ep.running = false
|
||||
close(ep.stop)
|
||||
}
|
||||
|
||||
func (ep *EmailProcessor) pollLoop() {
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
// Run immediately on start
|
||||
ep.processEmails()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if !ep.running {
|
||||
return
|
||||
}
|
||||
ep.processEmails()
|
||||
case <-ep.stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ep *EmailProcessor) processEmails() error {
|
||||
cfg, err := ep.db.GetEmailConfig()
|
||||
if err != nil {
|
||||
log.Printf("Email config error: %v", err)
|
||||
return err
|
||||
}
|
||||
if cfg == nil {
|
||||
log.Println("No email config found")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Connect to IMAP server
|
||||
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
|
||||
var c *client.Client
|
||||
|
||||
if cfg.SSL {
|
||||
c, err = client.DialTLS(addr, &tls.Config{InsecureSkipVerify: true})
|
||||
} else {
|
||||
c, err = client.Dial(addr)
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("IMAP connect error: %v", err)
|
||||
return err
|
||||
}
|
||||
defer c.Logout()
|
||||
|
||||
// Login
|
||||
if err := c.Login(cfg.Username, cfg.Password); err != nil {
|
||||
log.Printf("IMAP login error: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Select inbox
|
||||
mbox, err := c.Select(cfg.Folder, false)
|
||||
if err != nil {
|
||||
log.Printf("IMAP select error: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if mbox.Messages == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fetch unread messages
|
||||
criteria := imap.NewSearchCriteria()
|
||||
criteria.WithoutFlags = []string{imap.SeenFlag}
|
||||
uids, err := c.UidSearch(criteria)
|
||||
if err != nil {
|
||||
log.Printf("IMAP search error: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if len(uids) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, uid := range uids {
|
||||
ep.processMessage(c, uid)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ep *EmailProcessor) processMessage(c *client.Client, uid uint32) {
|
||||
seqSet := new(imap.SeqSet)
|
||||
seqSet.AddNum(uid)
|
||||
|
||||
section := &imap.BodySectionName{}
|
||||
items := []imap.FetchItem{section.FetchItem()}
|
||||
|
||||
messages := make(chan *imap.Message, 1)
|
||||
go func() {
|
||||
if err := c.UidFetch(seqSet, items, messages); err != nil {
|
||||
log.Printf("Fetch error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
msg := <-messages
|
||||
if msg == nil {
|
||||
return
|
||||
}
|
||||
|
||||
r := msg.GetBody(section)
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
log.Printf("Read error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse reservation from email
|
||||
reservation := ep.parseReservation(string(body))
|
||||
if reservation != nil {
|
||||
// Find available table
|
||||
tables, _ := ep.db.GetAllTables()
|
||||
for _, table := range tables {
|
||||
available, _ := ep.db.CheckAvailability(table.ID, reservation.Date, reservation.TimeFrom, reservation.TimeTo)
|
||||
if available && table.MaxGuests >= reservation.Guests {
|
||||
reservation.TableID = table.ID
|
||||
reservation.Source = "email"
|
||||
if err := ep.db.CreateReservation(reservation); err != nil {
|
||||
log.Printf("Create reservation error: %v", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark as seen
|
||||
seqSet2 := new(imap.SeqSet)
|
||||
seqSet2.AddNum(uid)
|
||||
c.UidStore(seqSet2, imap.FormatFlagsAdd, []string{imap.SeenFlag})
|
||||
}
|
||||
|
||||
func (ep *EmailProcessor) parseReservation(body string) *models.Reservation {
|
||||
res := &models.Reservation{}
|
||||
parsed := false
|
||||
|
||||
// Try to extract date
|
||||
datePatterns := []*regexp.Regexp{
|
||||
regexp.MustCompile(`(?i)(?:am|den)\s+(\d{1,2})[./](\d{1,2})[./](\d{2,4})`),
|
||||
regexp.MustCompile(`(?i)(\d{1,2})[./](\d{1,2})[./](\d{2,4})`),
|
||||
regexp.MustCompile(`(?i)(\d{4})-(\d{2})-(\d{2})`),
|
||||
}
|
||||
|
||||
for _, pattern := range datePatterns {
|
||||
if matches := pattern.FindStringSubmatch(body); matches != nil {
|
||||
if len(matches) >= 4 {
|
||||
if len(matches[3]) == 2 {
|
||||
res.Date = fmt.Sprintf("20%s-%s-%s", matches[3], matches[2], matches[1])
|
||||
} else {
|
||||
res.Date = fmt.Sprintf("%s-%s-%s", matches[3], matches[2], matches[1])
|
||||
}
|
||||
parsed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to extract time
|
||||
timePatterns := []*regexp.Regexp{
|
||||
regexp.MustCompile(`(?i)(\d{1,2})[:.](\d{2})\s*[Uu]hr`),
|
||||
regexp.MustCompile(`(?i)(\d{1,2})[:.](\d{2})`),
|
||||
regexp.MustCompile(`(?i)(\d{1,2})\s*[Uu]hr`),
|
||||
}
|
||||
|
||||
for _, pattern := range timePatterns {
|
||||
if matches := pattern.FindStringSubmatch(body); matches != nil {
|
||||
if len(matches) >= 2 {
|
||||
res.TimeFrom = fmt.Sprintf("%02s:00", matches[1])
|
||||
res.TimeTo = fmt.Sprintf("%02d:00", parseInt(matches[1])+2)
|
||||
parsed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to extract guest count
|
||||
guestPatterns := []*regexp.Regexp{
|
||||
regexp.MustCompile(`(?i)(\d+)\s*(?:Personen|Gäste|Leute)`),
|
||||
regexp.MustCompile(`(?i)für\s+(\d+)\s*Personen`),
|
||||
regexp.MustCompile(`(?i)(\d+)\s*Personen`),
|
||||
}
|
||||
|
||||
for _, pattern := range guestPatterns {
|
||||
if matches := pattern.FindStringSubmatch(body); matches != nil {
|
||||
res.Guests = parseInt(matches[1])
|
||||
parsed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Try to extract name
|
||||
namePatterns := []*regexp.Regexp{
|
||||
regexp.MustCompile(`(?i)Name\s*[:\-]?\s*([^\n\r]+)`),
|
||||
regexp.MustCompile(`(?i)(?:von|Name)\s+([A-Z][a-z]+\s+[A-Z][a-z]+)`),
|
||||
}
|
||||
|
||||
for _, pattern := range namePatterns {
|
||||
if matches := pattern.FindStringSubmatch(body); matches != nil {
|
||||
res.Name = strings.TrimSpace(matches[1])
|
||||
parsed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Try to extract phone
|
||||
phonePattern := regexp.MustCompile(`(?i)Tel(?:efon)?[:\s]*([\d\s\-\+\(\)]{7,})`)
|
||||
if matches := phonePattern.FindStringSubmatch(body); matches != nil {
|
||||
res.Phone = strings.TrimSpace(matches[1])
|
||||
}
|
||||
|
||||
// Try to extract email
|
||||
emailPattern := regexp.MustCompile(`[\w\.-]+@[\w\.-]+\.\w+`)
|
||||
if matches := emailPattern.FindStringSubmatch(body); matches != nil {
|
||||
res.Email = matches[0]
|
||||
}
|
||||
|
||||
// Try to extract notes
|
||||
notePatterns := []*regexp.Regexp{
|
||||
regexp.MustCompile(`(?i)Notiz(?:en)?\s*[:\-]?\s*([^\n\r]+)`),
|
||||
regexp.MustCompile(`(?i)Bemerkung(?:en)?\s*[:\-]?\s*([^\n\r]+)`),
|
||||
}
|
||||
|
||||
for _, pattern := range notePatterns {
|
||||
if matches := pattern.FindStringSubmatch(body); matches != nil {
|
||||
res.Notes = strings.TrimSpace(matches[1])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if parsed && res.Name != "" {
|
||||
return res
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseInt(s string) int {
|
||||
var n int
|
||||
fmt.Sscanf(s, "%d", &n)
|
||||
return n
|
||||
}
|
||||
@@ -0,0 +1,773 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Room struct {
|
||||
ID int `json:"id" db:"id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Capacity int `json:"capacity" db:"capacity"`
|
||||
Color string `json:"color" db:"color"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
type Table struct {
|
||||
ID int `json:"id" db:"id"`
|
||||
RoomID int `json:"room_id" db:"room_id"`
|
||||
X int `json:"x" db:"x"`
|
||||
Y int `json:"y" db:"y"`
|
||||
Width int `json:"width" db:"width"`
|
||||
Height int `json:"height" db:"height"`
|
||||
MaxGuests int `json:"max_guests" db:"max_guests"`
|
||||
Shape string `json:"shape" db:"shape"`
|
||||
Status string `json:"status" db:"status"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
RoomName string `json:"room_name,omitempty" db:"-"`
|
||||
}
|
||||
|
||||
type Reservation struct {
|
||||
ID int `json:"id" db:"id"`
|
||||
TableID int `json:"table_id" db:"table_id"`
|
||||
Date string `json:"date" db:"date"`
|
||||
TimeFrom string `json:"time_from" db:"time_from"`
|
||||
TimeTo string `json:"time_to" db:"time_to"`
|
||||
Guests int `json:"guests" db:"guests"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Phone string `json:"phone" db:"phone"`
|
||||
Email string `json:"email" db:"email"`
|
||||
Source string `json:"source" db:"source"`
|
||||
Notes string `json:"notes" db:"notes"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
TableName string `json:"table_name,omitempty" db:"-"`
|
||||
}
|
||||
|
||||
// NEW: RoomBooking for entire room reservations
|
||||
type RoomBooking struct {
|
||||
ID int `json:"id" db:"id"`
|
||||
RoomID int `json:"room_id" db:"room_id"`
|
||||
Date string `json:"date" db:"date"`
|
||||
TimeFrom string `json:"time_from" db:"time_from"`
|
||||
TimeTo string `json:"time_to" db:"time_to"`
|
||||
Guests int `json:"guests" db:"guests"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Phone string `json:"phone" db:"phone"`
|
||||
Email string `json:"email" db:"email"`
|
||||
EventType string `json:"event_type" db:"event_type"`
|
||||
Notes string `json:"notes" db:"notes"`
|
||||
Status string `json:"status" db:"status"` // confirmed, cancelled, pending
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
RoomName string `json:"room_name,omitempty" db:"-"`
|
||||
}
|
||||
|
||||
type TableMerge struct {
|
||||
ID int `json:"id" db:"id"`
|
||||
ParentTableID int `json:"parent_table_id" db:"parent_table_id"`
|
||||
ChildTableID int `json:"child_table_id" db:"child_table_id"`
|
||||
MergedName string `json:"merged_name" db:"merged_name"`
|
||||
Active bool `json:"active" db:"active"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
type EmailConfig struct {
|
||||
ID int `json:"id" db:"id"`
|
||||
Host string `json:"host" db:"host"`
|
||||
Port int `json:"port" db:"port"`
|
||||
Username string `json:"user" db:"user"`
|
||||
Password string `json:"password" db:"password"`
|
||||
SSL bool `json:"ssl" db:"ssl"`
|
||||
Folder string `json:"folder" db:"folder"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID int `json:"id" db:"id"`
|
||||
Username string `json:"username" db:"username"`
|
||||
Password string `json:"password" db:"password"`
|
||||
IsAdmin bool `json:"is_admin" db:"is_admin"`
|
||||
}
|
||||
|
||||
type Availability struct {
|
||||
TableID int `json:"table_id"`
|
||||
Date string `json:"date"`
|
||||
TimeFrom string `json:"time_from"`
|
||||
TimeTo string `json:"time_to"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// NEW: RoomAvailability for checking room availability
|
||||
type RoomAvailability struct {
|
||||
RoomID int `json:"room_id"`
|
||||
Date string `json:"date"`
|
||||
TimeFrom string `json:"time_from"`
|
||||
TimeTo string `json:"time_to"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Room struct {
|
||||
ID int `json:"id" db:"id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Capacity int `json:"capacity" db:"capacity"`
|
||||
Color string `json:"color" db:"color"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
type Table struct {
|
||||
ID int `json:"id" db:"id"`
|
||||
RoomID int `json:"room_id" db:"room_id"`
|
||||
X int `json:"x" db:"x"`
|
||||
Y int `json:"y" db:"y"`
|
||||
Width int `json:"width" db:"width"`
|
||||
Height int `json:"height" db:"height"`
|
||||
MaxGuests int `json:"max_guests" db:"max_guests"`
|
||||
Shape string `json:"shape" db:"shape"`
|
||||
Status string `json:"status" db:"status"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
RoomName string `json:"room_name,omitempty" db:"-"`
|
||||
}
|
||||
|
||||
type Reservation struct {
|
||||
ID int `json:"id" db:"id"`
|
||||
TableID int `json:"table_id" db:"table_id"`
|
||||
Date string `json:"date" db:"date"`
|
||||
TimeFrom string `json:"time_from" db:"time_from"`
|
||||
TimeTo string `json:"time_to" db:"time_to"`
|
||||
Guests int `json:"guests" db:"guests"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Phone string `json:"phone" db:"phone"`
|
||||
Email string `json:"email" db:"email"`
|
||||
Source string `json:"source" db:"source"`
|
||||
Notes string `json:"notes" db:"notes"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
TableName string `json:"table_name,omitempty" db:"-"`
|
||||
}
|
||||
|
||||
// NEW: RoomBooking for entire room reservations
|
||||
type RoomBooking struct {
|
||||
ID int `json:"id" db:"id"`
|
||||
RoomID int `json:"room_id" db:"room_id"`
|
||||
Date string `json:"date" db:"date"`
|
||||
TimeFrom string `json:"time_from" db:"time_from"`
|
||||
TimeTo string `json:"time_to" db:"time_to"`
|
||||
Guests int `json:"guests" db:"guests"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Phone string `json:"phone" db:"phone"`
|
||||
Email string `json:"email" db:"email"`
|
||||
EventType string `json:"event_type" db:"event_type"`
|
||||
Notes string `json:"notes" db:"notes"`
|
||||
Status string `json:"status" db:"status"` // confirmed, cancelled, pending
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
RoomName string `json:"room_name,omitempty" db:"-"`
|
||||
}
|
||||
|
||||
type TableMerge struct {
|
||||
ID int `json:"id" db:"id"`
|
||||
ParentTableID int `json:"parent_table_id" db:"parent_table_id"`
|
||||
ChildTableID int `json:"child_table_id" db:"child_table_id"`
|
||||
MergedName string `json:"merged_name" db:"merged_name"`
|
||||
Active bool `json:"active" db:"active"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
type EmailConfig struct {
|
||||
ID int `json:"id" db:"id"`
|
||||
Host string `json:"host" db:"host"`
|
||||
Port int `json:"port" db:"port"`
|
||||
Username string `json:"user" db:"user"`
|
||||
Password string `json:"password" db:"password"`
|
||||
SSL bool `json:"ssl" db:"ssl"`
|
||||
Folder string `json:"folder" db:"folder"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID int `json:"id" db:"id"`
|
||||
Username string `json:"username" db:"username"`
|
||||
Password string `json:"password" db:"password"`
|
||||
IsAdmin bool `json:"is_admin" db:"is_admin"`
|
||||
}
|
||||
|
||||
type Availability struct {
|
||||
TableID int `json:"table_id"`
|
||||
Date string `json:"date"`
|
||||
TimeFrom string `json:"time_from"`
|
||||
TimeTo string `json:"time_to"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// NEW: RoomAvailability for checking room availability
|
||||
type RoomAvailability struct {
|
||||
RoomID int `json:"room_id"`
|
||||
Date string `json:"date"`
|
||||
TimeFrom string `json:"time_from"`
|
||||
TimeTo string `json:"time_to"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
Reference in New Issue
Block a user