Skip to content

Commit f2c2774

Browse files
link guests frontend to backend [FIXED] (#330)
* feat: link frontend to backend for guests * fix: Update group size filtering, date rendering, added tests * style: Fix linting * feat: Clean up code, add sorting on guest stays, notes handling * style: Fix styling * style: Fix styling * feat: fix the stay-history scan, sort stay slices * feat: Update guestsContent with a plain if statement * feat: Wire filters to backend, update guests list display * feat: Persist notes * feat: fix failed to get get guests bug * style: fix styling * style: fix styling * fix: fix failing test * feat: Update param names and fix assistance marshalling * fix: remove duplicate hotelID * fix: Remove extra tick * fix: Update swagger * fix: Update hook used to be compatible with orval * fix: pls work * style: fix style * feat: Add swagger fixes
1 parent 3e726b3 commit f2c2774

File tree

19 files changed

+608
-298
lines changed

19 files changed

+608
-298
lines changed

backend/docs/swagger.yaml

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,6 @@ definitions:
200200
items:
201201
type: integer
202202
type: array
203-
hotel_id:
204-
type: string
205203
limit:
206204
maximum: 100
207205
minimum: 1
@@ -219,8 +217,6 @@ definitions:
219217
items:
220218
$ref: '#/definitions/github_com_generate_selfserve_internal_models.BookingStatus'
221219
type: array
222-
required:
223-
- hotel_id
224220
type: object
225221
GuestPage:
226222
properties:
@@ -259,25 +255,32 @@ definitions:
259255
GuestWithBooking:
260256
properties:
261257
first_name:
258+
example: Jane
262259
type: string
263260
floor:
261+
example: 3
264262
type: integer
265263
group_size:
264+
example: 2
266265
type: integer
267266
id:
267+
example: 530e8400-e458-41d4-a716-446655440000
268268
type: string
269269
last_name:
270+
example: Doe
270271
type: string
271272
preferred_name:
273+
example: Jane
272274
type: string
273275
room_number:
276+
example: 301
274277
type: integer
275278
required:
276279
- first_name
277280
- floor
278-
- group_size
279281
- id
280282
- last_name
283+
- preferred_name
281284
- room_number
282285
type: object
283286
GuestWithStays:
@@ -1007,6 +1010,7 @@ paths:
10071010
consumes:
10081011
- application/json
10091012
description: Retrieves a single guest with previous stays given an id
1013+
operationId: getGuestsStaysId
10101014
parameters:
10111015
- description: Guest ID (UUID)
10121016
in: path

backend/internal/handler/guests.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ func (h *GuestsHandler) GetGuest(c *fiber.Ctx) error {
102102
// @Failure 400 {object} map[string]string "Invalid guest ID format"
103103
// @Failure 404 {object} errs.HTTPError "Guest not found"
104104
// @Failure 500 {object} map[string]string "Internal server error"
105+
// @ID getGuestsStaysId
105106
// @Security BearerAuth
106107
// @Router /guests/stays/{id} [get]
107108
func (h *GuestsHandler) GetGuestWithStays(c *fiber.Ctx) error {
@@ -116,6 +117,7 @@ func (h *GuestsHandler) GetGuestWithStays(c *fiber.Ctx) error {
116117
return errs.NotFound("guest", "id", id)
117118

118119
}
120+
slog.Error("failed to get guest with stays", "id", id, "error", err)
119121
return errs.InternalServerError()
120122
}
121123
return c.JSON(guest)

backend/internal/models/guests.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ const (
7979
)
8080

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

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

112112
type GuestWithStays struct {
@@ -118,8 +118,8 @@ type GuestWithStays struct {
118118
Preferences *string `json:"preferences,omitempty" example:"extra pillows"`
119119
Notes *string `json:"notes,omitempty" example:"VIP"`
120120
Pronouns *string `json:"pronouns,omitempty" example:"she/her"`
121-
DoNotDisturbStart *time.Time `json:"do_not_disturb_start,omitempty" example:"17:00:00"`
122-
DoNotDisturbEnd *time.Time `json:"do_not_disturb_end,omitempty" example:"07:00:00"`
121+
DoNotDisturbStart *string `json:"do_not_disturb_start,omitempty" example:"17:00:00"`
122+
DoNotDisturbEnd *string `json:"do_not_disturb_end,omitempty" example:"07:00:00"`
123123
HousekeepingCadence *string `json:"housekeeping_cadence,omitempty" example:"daily"`
124124
Assistance *Assistance `json:"assistance,omitempty"`
125125
CurrentStays []Stay `json:"current_stays" validate:"required"`

backend/internal/repository/guests.go

Lines changed: 86 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,19 @@ package repository
22

33
import (
44
"context"
5+
"encoding/json"
56
"errors"
7+
"fmt"
68
"iter"
9+
"sort"
710
"strings"
811
"time"
912

1013
"github.com/generate/selfserve/internal/errs"
1114
"github.com/generate/selfserve/internal/models"
1215
"github.com/jackc/pgx/v5"
1316
"github.com/jackc/pgx/v5/pgconn"
17+
"github.com/jackc/pgx/v5/pgtype"
1418
"github.com/jackc/pgx/v5/pgxpool"
1519
)
1620

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

8791
func (r *GuestsRepository) FindGuestWithStayHistory(ctx context.Context, id string) (*models.GuestWithStays, error) {
88-
guest := &models.GuestWithStays{}
92+
guest := &models.GuestWithStays{
93+
CurrentStays: []models.Stay{},
94+
PastStays: []models.Stay{},
95+
}
96+
var doNotDisturbStart, doNotDisturbEnd pgtype.Time
97+
var assistanceRaw []byte
8998

9099
err := r.db.QueryRow(ctx, `
91100
SELECT
@@ -96,8 +105,8 @@ func (r *GuestsRepository) FindGuestWithStayHistory(ctx context.Context, id stri
96105
WHERE g.id = $1
97106
`, id).Scan(
98107
&guest.ID, &guest.FirstName, &guest.LastName, &guest.Phone, &guest.Email,
99-
&guest.Preferences, &guest.Notes, &guest.Pronouns, &guest.DoNotDisturbStart,
100-
&guest.DoNotDisturbEnd, &guest.HousekeepingCadence, &guest.Assistance,
108+
&guest.Preferences, &guest.Notes, &guest.Pronouns, &doNotDisturbStart,
109+
&doNotDisturbEnd, &guest.HousekeepingCadence, &assistanceRaw,
101110
)
102111
if err != nil {
103112
if errors.Is(err, pgx.ErrNoRows) {
@@ -106,6 +115,17 @@ func (r *GuestsRepository) FindGuestWithStayHistory(ctx context.Context, id stri
106115
return nil, err
107116
}
108117

118+
guest.DoNotDisturbStart = formatPGTime(doNotDisturbStart)
119+
guest.DoNotDisturbEnd = formatPGTime(doNotDisturbEnd)
120+
121+
if len(assistanceRaw) > 0 {
122+
var assistance *models.Assistance
123+
if err := json.Unmarshal(assistanceRaw, &assistance); err != nil {
124+
return nil, err
125+
}
126+
guest.Assistance = assistance
127+
}
128+
109129
rows, err := r.db.Query(ctx, `
110130
SELECT gb.arrival_date, gb.departure_date, rm.room_number, gb.status, gb.group_size
111131
FROM guest_bookings gb
@@ -117,35 +137,53 @@ func (r *GuestsRepository) FindGuestWithStayHistory(ctx context.Context, id stri
117137
}
118138
defer rows.Close()
119139

140+
if err := loadGuestStayHistory(guest, rows); err != nil {
141+
return nil, err
142+
}
143+
144+
sortGuestStays(guest)
145+
146+
return guest, rows.Err()
147+
}
148+
149+
func loadGuestStayHistory(guest *models.GuestWithStays, rows pgx.Rows) error {
120150
for rows.Next() {
121-
var arrivalDate, departureDate *time.Time
122-
var roomNumber, groupSize *int
123-
var status *models.BookingStatus
151+
var arrivalDate, departureDate pgtype.Date
152+
var roomNumber, groupSize pgtype.Int4
153+
var status string
124154

125155
if err := rows.Scan(&arrivalDate, &departureDate, &roomNumber, &status, &groupSize); err != nil {
126-
return nil, err
156+
return err
127157
}
128158

129-
if arrivalDate == nil {
159+
if !arrivalDate.Valid || !departureDate.Valid || !roomNumber.Valid || status == "" {
130160
continue
131161
}
132162

133-
stay := buildStay(arrivalDate, departureDate, roomNumber, groupSize, status)
134-
guest = appendStay(guest, stay, *status)
163+
stayStatus := models.BookingStatus(status)
164+
stay := buildStay(arrivalDate, departureDate, roomNumber, groupSize, stayStatus)
165+
appendStay(guest, stay, stayStatus)
135166
}
136167

137-
return guest, rows.Err()
168+
return rows.Err()
138169
}
139170

140-
func buildStay(arrival, departure *time.Time, roomNumber, groupSize *int, status *models.BookingStatus) models.Stay {
171+
func buildStay(
172+
arrival pgtype.Date,
173+
departure pgtype.Date,
174+
roomNumber pgtype.Int4,
175+
groupSize pgtype.Int4,
176+
status models.BookingStatus,
177+
) models.Stay {
141178
stay := models.Stay{
142-
ArrivalDate: *arrival,
143-
DepartureDate: *departure,
144-
RoomNumber: *roomNumber,
145-
Status: *status,
179+
ArrivalDate: arrival.Time,
180+
DepartureDate: departure.Time,
181+
RoomNumber: int(roomNumber.Int32),
182+
Status: status,
146183
}
147-
if groupSize != nil {
148-
stay.GroupSize = groupSize
184+
if groupSize.Valid {
185+
value := int(groupSize.Int32)
186+
stay.GroupSize = &value
149187
}
150188
return stay
151189
}
@@ -160,6 +198,36 @@ func appendStay(guest *models.GuestWithStays, stay models.Stay, status models.Bo
160198
return guest
161199
}
162200

201+
func sortGuestStays(guest *models.GuestWithStays) {
202+
sort.Slice(guest.CurrentStays, func(currentStayIndex, otherCurrentStayIndex int) bool {
203+
return guest.CurrentStays[currentStayIndex].ArrivalDate.After(
204+
guest.CurrentStays[otherCurrentStayIndex].ArrivalDate,
205+
)
206+
})
207+
208+
sort.Slice(guest.PastStays, func(pastStayIndex, otherPastStayIndex int) bool {
209+
return guest.PastStays[pastStayIndex].DepartureDate.After(
210+
guest.PastStays[otherPastStayIndex].DepartureDate,
211+
)
212+
})
213+
}
214+
215+
func formatPGTime(value pgtype.Time) *string {
216+
if !value.Valid {
217+
return nil
218+
}
219+
220+
duration := time.Duration(value.Microseconds) * time.Microsecond
221+
hours := int(duration / time.Hour)
222+
duration -= time.Duration(hours) * time.Hour
223+
minutes := int(duration / time.Minute)
224+
duration -= time.Duration(minutes) * time.Minute
225+
seconds := int(duration / time.Second)
226+
227+
formatted := fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds)
228+
return &formatted
229+
}
230+
163231
func (r *GuestsRepository) UpdateGuest(ctx context.Context, id string, update *models.UpdateGuest) (*models.Guest, error) {
164232
var guest models.Guest
165233

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package repository
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/generate/selfserve/internal/models"
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestSortGuestStays(t *testing.T) {
12+
t.Parallel()
13+
14+
base := time.Date(2026, time.April, 4, 12, 0, 0, 0, time.UTC)
15+
guest := &models.GuestWithStays{
16+
CurrentStays: []models.Stay{
17+
{
18+
ArrivalDate: base.Add(-48 * time.Hour),
19+
DepartureDate: base.Add(24 * time.Hour),
20+
RoomNumber: 101,
21+
Status: models.BookingStatusActive,
22+
},
23+
{
24+
ArrivalDate: base.Add(-24 * time.Hour),
25+
DepartureDate: base.Add(48 * time.Hour),
26+
RoomNumber: 202,
27+
Status: models.BookingStatusActive,
28+
},
29+
},
30+
PastStays: []models.Stay{
31+
{
32+
ArrivalDate: base.Add(-240 * time.Hour),
33+
DepartureDate: base.Add(-168 * time.Hour),
34+
RoomNumber: 303,
35+
Status: models.BookingStatusInactive,
36+
},
37+
{
38+
ArrivalDate: base.Add(-120 * time.Hour),
39+
DepartureDate: base.Add(-72 * time.Hour),
40+
RoomNumber: 404,
41+
Status: models.BookingStatusInactive,
42+
},
43+
},
44+
}
45+
46+
sortGuestStays(guest)
47+
48+
assert.Equal(t, 202, guest.CurrentStays[0].RoomNumber)
49+
assert.Equal(t, 101, guest.CurrentStays[1].RoomNumber)
50+
assert.Equal(t, 404, guest.PastStays[0].RoomNumber)
51+
assert.Equal(t, 303, guest.PastStays[1].RoomNumber)
52+
}

clients/shared/src/index.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,19 @@ export {
5151
usePostGuests,
5252
useGetGuestsId,
5353
usePutGuestsId,
54+
usePostGuestsSearchHook,
55+
useGetGuestsStaysId,
5456
} from "./api/generated/endpoints/guests/guests";
5557

56-
export { useGetGuestBookingsGroupSizes } from "./api/generated/endpoints/guest-bookings/guest-bookings";
58+
export type {
59+
GuestWithBooking,
60+
GuestWithStays,
61+
GuestFilters,
62+
Stay,
63+
} from "./api/generated/models";
5764

58-
export {
59-
usePostRooms,
60-
useGetRoomsFloors,
61-
} from "./api/generated/endpoints/rooms/rooms";
65+
export { usePostRooms, useGetRoomsFloors } from "./api/generated/endpoints/rooms/rooms";
66+
export { useGetGuestBookingsGroupSizes } from "./api/generated/endpoints/guest-bookings/guest-bookings";
6267

6368
export type {
6469
RoomWithOptionalGuestBooking,

0 commit comments

Comments
 (0)