v2.0: 3-Raum-System - Hauptraum, Saal A, Saal B mit 18 Tischen, Raum-Buchungen, API-Doku

This commit is contained in:
Peter
2026-05-16 12:15:46 +00:00
commit 1ae070f82f
35 changed files with 10640 additions and 0 deletions
+773
View File
@@ -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)
}
+53
View File
@@ -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")
}
+616
View File
@@ -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
}
+616
View File
@@ -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
}
+290
View File
@@ -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
}
+773
View File
@@ -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)
}
+104
View File
@@ -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"`
}
+104
View File
@@ -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"`
}