feat: reviews module — email request after checkout, API, job

- Migration 084: add review_token, review_email_sent_at, review_retry_sent_at to bookings
- Backend route /api/hotels/:slug/reviews (GET list, PATCH reply/status)
- Backend public routes /api/public/review/:token (GET config, POST submit)
- email.ts: sendReviewRequestEmail template
- jobs.ts: runReviewRequestJob — sends email N hours after checkout, retry after 24h
- app.ts: register reviewsRoutes
- api.ts: add ReviewApi, PublicReviewConfig types + reviews/publicReview methods
- ReviewsPage: connect to real API, load/save settings, remove SMS option
- GuestReviewPage: use :token param, fetch config from API, submit to API
- App.tsx: route /review/:slug → /review/:token

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-21 00:04:59 +03:00
parent 696cf4d38c
commit 1ab2751854
9 changed files with 687 additions and 353 deletions

View File

@@ -477,6 +477,24 @@ export const api = {
req<{ ok: boolean }>('PATCH', `/api/hotels/${slug}/hotel-settings`, data),
},
// ── Reviews ───────────────────────────────────────────────────────────────
reviews: {
list: (slug: string) =>
req<ReviewApi[]>('GET', `/api/hotels/${slug}/reviews`),
update: (slug: string, id: string, data: { reply?: string; isPublic?: boolean }) =>
req<ReviewApi>('PATCH', `/api/hotels/${slug}/reviews/${id}`, data),
},
// ── Public review (no auth) ───────────────────────────────────────────────
publicReview: {
getConfig: (token: string) =>
req<PublicReviewConfig>('GET', `/api/public/review/${token}`),
submit: (token: string, data: { rating: number; text?: string }) =>
req<{ ok: boolean }>('POST', `/api/public/review/${token}`, data),
},
// ── Guests ────────────────────────────────────────────────────────────────
guests: {
list: (slug: string, q?: string, passport?: string) => {
@@ -1843,3 +1861,33 @@ function toHotelPayload(h: HotelPayload): Record<string, unknown> {
if (h.checkOutTime !== undefined) out.check_out_time = h.checkOutTime
return out
}
export interface ReviewApi {
id: string
bookingId: string | null
guestId: string | null
guestName: string
source: string
rating: number
text: string | null
reply: string | null
repliedAt: string | null
isPublic: boolean
roomNumber: string | null
bookingSource: string | null
createdAt: string
updatedAt: string
}
export interface PublicReviewConfig {
hotelName: string
logoLetter: string
welcomeText: string
color: string
redirectThreshold: number
showTextField: boolean
platforms: { name: string; url: string }[]
guestName: string
bookingSource: string
}