Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
1af0eb1
feat: link frontend to backend for guests
zaydaanjahangir Mar 27, 2026
ec72610
fix: Update group size filtering, date rendering, added tests
zaydaanjahangir Mar 31, 2026
eb2c315
Merge remote-tracking branch 'origin/main' into feat/129-link-geusts-…
zaydaanjahangir Mar 31, 2026
fb5acc9
style: Fix linting
zaydaanjahangir Mar 31, 2026
75fe730
Merge remote-tracking branch 'origin/main' into feat/129-link-geusts-…
zaydaanjahangir Apr 1, 2026
e1a7e85
Merge remote-tracking branch 'origin/main' into feat/129-link-geusts-…
zaydaanjahangir Apr 1, 2026
586f926
feat: Clean up code, add sorting on guest stays, notes handling
zaydaanjahangir Apr 4, 2026
d3b67d3
Merge branch 'main' into feat/129-link-geusts-frontend-to-backend
zaydaanjahangir Apr 4, 2026
410418f
style: Fix styling
zaydaanjahangir Apr 4, 2026
8dd6492
Merge branch 'feat/129-link-geusts-frontend-to-backend' of https://gi…
zaydaanjahangir Apr 4, 2026
e36b3f9
style: Fix styling
zaydaanjahangir Apr 4, 2026
3303821
Merge branch 'main' into feat/129-link-geusts-frontend-to-backend
zaydaanjahangir Apr 4, 2026
4a918d2
Merge remote-tracking branch 'origin/main' into feat/129-link-geusts-…
zaydaanjahangir Apr 9, 2026
cb15a74
feat: fix the stay-history scan, sort stay slices
zaydaanjahangir Apr 9, 2026
0c1cb80
feat: Update guestsContent with a plain if statement
zaydaanjahangir Apr 9, 2026
da8494a
feat: Wire filters to backend, update guests list display
zaydaanjahangir Apr 9, 2026
3c59258
feat: Persist notes
zaydaanjahangir Apr 9, 2026
cfd8367
feat: fix failed to get get guests bug
zaydaanjahangir Apr 9, 2026
9ad49e5
style: fix styling
zaydaanjahangir Apr 9, 2026
4c599fe
style: fix styling
zaydaanjahangir Apr 9, 2026
bf4c3ba
fix: fix failing test
zaydaanjahangir Apr 9, 2026
5761385
feat: Update param names and fix assistance marshalling
zaydaanjahangir Apr 10, 2026
3a1bc37
Merge branch 'main' into feat/129-link-geusts-frontend-to-backend
zaydaanjahangir Apr 10, 2026
096b961
fix: remove duplicate hotelID
zaydaanjahangir Apr 10, 2026
06514b3
fix: Remove extra tick
zaydaanjahangir Apr 10, 2026
4fe65e1
fix: Update swagger
zaydaanjahangir Apr 10, 2026
50080a3
Merge branch 'main' into feat/129-link-geusts-frontend-to-backend
zaydaanjahangir Apr 10, 2026
6966c29
fix: Update hook used to be compatible with orval
zaydaanjahangir Apr 10, 2026
4342d3c
Merge branch 'feat/129-link-geusts-frontend-to-backend' of https://gi…
zaydaanjahangir Apr 10, 2026
64061dc
fix: pls work
zaydaanjahangir Apr 10, 2026
1072903
style: fix style
zaydaanjahangir Apr 10, 2026
cdf8f39
Merge remote-tracking branch 'origin/main' into feat/129-link-geusts-…
zaydaanjahangir Apr 10, 2026
e33b77b
Merge remote-tracking branch 'origin/main' into feat/129-link-geusts-…
zaydaanjahangir Apr 13, 2026
369ae33
feat: Add swagger fixes
zaydaanjahangir Apr 13, 2026
fe22496
Merge branch 'main' into feat/129-link-geusts-frontend-to-backend
zaydaanjahangir Apr 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions backend/docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,6 @@ definitions:
items:
type: integer
type: array
hotel_id:
type: string
limit:
maximum: 100
minimum: 1
Expand All @@ -219,8 +217,6 @@ definitions:
items:
$ref: '#/definitions/github_com_generate_selfserve_internal_models.BookingStatus'
type: array
required:
- hotel_id
type: object
GuestPage:
properties:
Expand Down Expand Up @@ -259,25 +255,32 @@ definitions:
GuestWithBooking:
properties:
first_name:
example: Jane
type: string
floor:
example: 3
type: integer
group_size:
example: 2
type: integer
id:
example: 530e8400-e458-41d4-a716-446655440000
type: string
last_name:
example: Doe
type: string
preferred_name:
example: Jane
type: string
room_number:
example: 301
type: integer
required:
- first_name
- floor
- group_size
- id
- last_name
- preferred_name
- room_number
type: object
GuestWithStays:
Expand Down Expand Up @@ -1007,6 +1010,7 @@ paths:
consumes:
- application/json
description: Retrieves a single guest with previous stays given an id
operationId: getGuestsStaysId
parameters:
- description: Guest ID (UUID)
in: path
Expand Down
2 changes: 2 additions & 0 deletions backend/internal/handler/guests.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ func (h *GuestsHandler) GetGuest(c *fiber.Ctx) error {
// @Failure 400 {object} map[string]string "Invalid guest ID format"
// @Failure 404 {object} errs.HTTPError "Guest not found"
// @Failure 500 {object} map[string]string "Internal server error"
// @ID getGuestsStaysId
// @Security BearerAuth
// @Router /guests/stays/{id} [get]
func (h *GuestsHandler) GetGuestWithStays(c *fiber.Ctx) error {
Expand All @@ -116,6 +117,7 @@ func (h *GuestsHandler) GetGuestWithStays(c *fiber.Ctx) error {
return errs.NotFound("guest", "id", id)

}
slog.Error("failed to get guest with stays", "id", id, "error", err)
return errs.InternalServerError()
}
return c.JSON(guest)
Expand Down
20 changes: 10 additions & 10 deletions backend/internal/models/guests.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ const (
)

type GuestFilters struct {
HotelID string `json:"hotel_id" validate:"required,startswith=org_"`
HotelID string `json:"hotel_id" validate:"required,startswith=org_" swaggerignore:"true"`
Status []BookingStatus `json:"status" validate:"omitempty,dive,oneof=active inactive"`
BookingSort GuestSortOrder `json:"booking_sort" validate:"omitempty,oneof=high_to_low low_to_high"`
RequestSort RequestSortOrder `json:"request_sort" validate:"omitempty,oneof=high_to_low low_to_high urgent"`
Expand All @@ -100,13 +100,13 @@ type GuestPage struct {
} // @name GuestPage

type GuestWithBooking struct {
ID string `json:"id" validate:"required"`
FirstName string `json:"first_name" validate:"required"`
LastName string `json:"last_name" validate:"required"`
PreferredName string `json:"preferred_name"`
Floor int `json:"floor" validate:"required"`
RoomNumber int `json:"room_number" validate:"required"`
GroupSize *int `json:"group_size" validate:"required"`
ID string `json:"id" validate:"required" example:"530e8400-e458-41d4-a716-446655440000"`
FirstName string `json:"first_name" validate:"required" example:"Jane"`
LastName string `json:"last_name" validate:"required" example:"Doe"`
PreferredName string `json:"preferred_name" validate:"required" example:"Jane"`
Floor int `json:"floor" validate:"required" example:"3"`
RoomNumber int `json:"room_number" validate:"required" example:"301"`
GroupSize *int `json:"group_size" example:"2"`
} // @name GuestWithBooking

type GuestWithStays struct {
Expand All @@ -118,8 +118,8 @@ type GuestWithStays struct {
Preferences *string `json:"preferences,omitempty" example:"extra pillows"`
Notes *string `json:"notes,omitempty" example:"VIP"`
Pronouns *string `json:"pronouns,omitempty" example:"she/her"`
DoNotDisturbStart *time.Time `json:"do_not_disturb_start,omitempty" example:"17:00:00"`
DoNotDisturbEnd *time.Time `json:"do_not_disturb_end,omitempty" example:"07:00:00"`
DoNotDisturbStart *string `json:"do_not_disturb_start,omitempty" example:"17:00:00"`
DoNotDisturbEnd *string `json:"do_not_disturb_end,omitempty" example:"07:00:00"`
HousekeepingCadence *string `json:"housekeeping_cadence,omitempty" example:"daily"`
Assistance *Assistance `json:"assistance,omitempty"`
CurrentStays []Stay `json:"current_stays" validate:"required"`
Expand Down
104 changes: 86 additions & 18 deletions backend/internal/repository/guests.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@ package repository

import (
"context"
"encoding/json"
"errors"
"fmt"
"iter"
"sort"
"strings"
"time"

"github.com/generate/selfserve/internal/errs"
"github.com/generate/selfserve/internal/models"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
)

Expand Down Expand Up @@ -85,7 +89,12 @@ func (r *GuestsRepository) FindGuest(ctx context.Context, id string) (*models.Gu
}

func (r *GuestsRepository) FindGuestWithStayHistory(ctx context.Context, id string) (*models.GuestWithStays, error) {
guest := &models.GuestWithStays{}
guest := &models.GuestWithStays{
CurrentStays: []models.Stay{},
PastStays: []models.Stay{},
}
var doNotDisturbStart, doNotDisturbEnd pgtype.Time
var assistanceRaw []byte

err := r.db.QueryRow(ctx, `
SELECT
Expand All @@ -96,8 +105,8 @@ func (r *GuestsRepository) FindGuestWithStayHistory(ctx context.Context, id stri
WHERE g.id = $1
`, id).Scan(
&guest.ID, &guest.FirstName, &guest.LastName, &guest.Phone, &guest.Email,
&guest.Preferences, &guest.Notes, &guest.Pronouns, &guest.DoNotDisturbStart,
&guest.DoNotDisturbEnd, &guest.HousekeepingCadence, &guest.Assistance,
&guest.Preferences, &guest.Notes, &guest.Pronouns, &doNotDisturbStart,
&doNotDisturbEnd, &guest.HousekeepingCadence, &assistanceRaw,
)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
Expand All @@ -106,6 +115,17 @@ func (r *GuestsRepository) FindGuestWithStayHistory(ctx context.Context, id stri
return nil, err
}

guest.DoNotDisturbStart = formatPGTime(doNotDisturbStart)
guest.DoNotDisturbEnd = formatPGTime(doNotDisturbEnd)

if len(assistanceRaw) > 0 {
var assistance *models.Assistance
if err := json.Unmarshal(assistanceRaw, &assistance); err != nil {
return nil, err
}
guest.Assistance = assistance
}

rows, err := r.db.Query(ctx, `
SELECT gb.arrival_date, gb.departure_date, rm.room_number, gb.status, gb.group_size
FROM guest_bookings gb
Expand All @@ -117,35 +137,53 @@ func (r *GuestsRepository) FindGuestWithStayHistory(ctx context.Context, id stri
}
defer rows.Close()

if err := loadGuestStayHistory(guest, rows); err != nil {
return nil, err
}

sortGuestStays(guest)

return guest, rows.Err()
}

func loadGuestStayHistory(guest *models.GuestWithStays, rows pgx.Rows) error {
for rows.Next() {
var arrivalDate, departureDate *time.Time
var roomNumber, groupSize *int
var status *models.BookingStatus
var arrivalDate, departureDate pgtype.Date
var roomNumber, groupSize pgtype.Int4
var status string

if err := rows.Scan(&arrivalDate, &departureDate, &roomNumber, &status, &groupSize); err != nil {
return nil, err
return err
}

if arrivalDate == nil {
if !arrivalDate.Valid || !departureDate.Valid || !roomNumber.Valid || status == "" {
continue
}

stay := buildStay(arrivalDate, departureDate, roomNumber, groupSize, status)
guest = appendStay(guest, stay, *status)
stayStatus := models.BookingStatus(status)
stay := buildStay(arrivalDate, departureDate, roomNumber, groupSize, stayStatus)
appendStay(guest, stay, stayStatus)
}

return guest, rows.Err()
return rows.Err()
}

func buildStay(arrival, departure *time.Time, roomNumber, groupSize *int, status *models.BookingStatus) models.Stay {
func buildStay(
arrival pgtype.Date,
departure pgtype.Date,
roomNumber pgtype.Int4,
groupSize pgtype.Int4,
status models.BookingStatus,
) models.Stay {
stay := models.Stay{
ArrivalDate: *arrival,
DepartureDate: *departure,
RoomNumber: *roomNumber,
Status: *status,
ArrivalDate: arrival.Time,
DepartureDate: departure.Time,
RoomNumber: int(roomNumber.Int32),
Status: status,
}
if groupSize != nil {
stay.GroupSize = groupSize
if groupSize.Valid {
value := int(groupSize.Int32)
stay.GroupSize = &value
}
return stay
}
Expand All @@ -160,6 +198,36 @@ func appendStay(guest *models.GuestWithStays, stay models.Stay, status models.Bo
return guest
}

func sortGuestStays(guest *models.GuestWithStays) {
sort.Slice(guest.CurrentStays, func(currentStayIndex, otherCurrentStayIndex int) bool {
return guest.CurrentStays[currentStayIndex].ArrivalDate.After(
guest.CurrentStays[otherCurrentStayIndex].ArrivalDate,
)
})

sort.Slice(guest.PastStays, func(pastStayIndex, otherPastStayIndex int) bool {
return guest.PastStays[pastStayIndex].DepartureDate.After(
guest.PastStays[otherPastStayIndex].DepartureDate,
)
})
}

func formatPGTime(value pgtype.Time) *string {
if !value.Valid {
return nil
}

duration := time.Duration(value.Microseconds) * time.Microsecond
hours := int(duration / time.Hour)
duration -= time.Duration(hours) * time.Hour
minutes := int(duration / time.Minute)
duration -= time.Duration(minutes) * time.Minute
seconds := int(duration / time.Second)

formatted := fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds)
return &formatted
}

func (r *GuestsRepository) UpdateGuest(ctx context.Context, id string, update *models.UpdateGuest) (*models.Guest, error) {
var guest models.Guest

Expand Down
52 changes: 52 additions & 0 deletions backend/internal/repository/guests_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package repository

import (
"testing"
"time"

"github.com/generate/selfserve/internal/models"
"github.com/stretchr/testify/assert"
)

func TestSortGuestStays(t *testing.T) {
t.Parallel()

base := time.Date(2026, time.April, 4, 12, 0, 0, 0, time.UTC)
guest := &models.GuestWithStays{
CurrentStays: []models.Stay{
{
ArrivalDate: base.Add(-48 * time.Hour),
DepartureDate: base.Add(24 * time.Hour),
RoomNumber: 101,
Status: models.BookingStatusActive,
},
{
ArrivalDate: base.Add(-24 * time.Hour),
DepartureDate: base.Add(48 * time.Hour),
RoomNumber: 202,
Status: models.BookingStatusActive,
},
},
PastStays: []models.Stay{
{
ArrivalDate: base.Add(-240 * time.Hour),
DepartureDate: base.Add(-168 * time.Hour),
RoomNumber: 303,
Status: models.BookingStatusInactive,
},
{
ArrivalDate: base.Add(-120 * time.Hour),
DepartureDate: base.Add(-72 * time.Hour),
RoomNumber: 404,
Status: models.BookingStatusInactive,
},
},
}

sortGuestStays(guest)

assert.Equal(t, 202, guest.CurrentStays[0].RoomNumber)
assert.Equal(t, 101, guest.CurrentStays[1].RoomNumber)
assert.Equal(t, 404, guest.PastStays[0].RoomNumber)
assert.Equal(t, 303, guest.PastStays[1].RoomNumber)
}
15 changes: 10 additions & 5 deletions clients/shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,19 @@ export {
usePostGuests,
useGetGuestsId,
usePutGuestsId,
usePostGuestsSearchHook,
useGetGuestsStaysId,
} from "./api/generated/endpoints/guests/guests";

export { useGetGuestBookingsGroupSizes } from "./api/generated/endpoints/guest-bookings/guest-bookings";
export type {
GuestWithBooking,
GuestWithStays,
GuestFilters,
Stay,
} from "./api/generated/models";

export {
usePostRooms,
useGetRoomsFloors,
} from "./api/generated/endpoints/rooms/rooms";
export { usePostRooms, useGetRoomsFloors } from "./api/generated/endpoints/rooms/rooms";
export { useGetGuestBookingsGroupSizes } from "./api/generated/endpoints/guest-bookings/guest-bookings";

export type {
RoomWithOptionalGuestBooking,
Expand Down
Loading
Loading