From 9bda48c0203c01a6698579e807ee7c3c45f3e9a6 Mon Sep 17 00:00:00 2001 From: HotelSync Date: Tue, 21 Apr 2026 17:43:49 +0300 Subject: [PATCH] feat: guest email field on QR review form for reply Co-Authored-By: Claude Sonnet 4.6 --- .../migrations/091_reviews_guest_email.sql | 4 +++ backend/src/routes/reviews.ts | 15 +++++----- src/lib/api.ts | 1 + src/pages/GuestReviewQrPage.tsx | 13 +++++++- src/pages/ReviewsPage.tsx | 30 ++++++++++--------- 5 files changed, 41 insertions(+), 22 deletions(-) create mode 100644 backend/migrations/091_reviews_guest_email.sql diff --git a/backend/migrations/091_reviews_guest_email.sql b/backend/migrations/091_reviews_guest_email.sql new file mode 100644 index 0000000..88202bd --- /dev/null +++ b/backend/migrations/091_reviews_guest_email.sql @@ -0,0 +1,4 @@ +-- Migration 091 — Add guest_email to reviews (for QR anonymous reviews) + +ALTER TABLE reviews + ADD COLUMN IF NOT EXISTS guest_email VARCHAR(255); diff --git a/backend/src/routes/reviews.ts b/backend/src/routes/reviews.ts index 77d5230..723a175 100644 --- a/backend/src/routes/reviews.ts +++ b/backend/src/routes/reviews.ts @@ -38,7 +38,7 @@ const reviewsRoutes: FastifyPluginAsync = async (fastify) => { if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' }) const { rows } = await db.query( - `SELECT r.id, r.booking_id, r.guest_id, r.guest_name, r.source, + `SELECT r.id, r.booking_id, r.guest_id, r.guest_name, r.guest_email, r.source, r.rating, r.text, r.reply, r.replied_at, r.is_public, r.rejected_at, r.created_at, r.updated_at, b.room_id, @@ -94,8 +94,8 @@ const reviewsRoutes: FastifyPluginAsync = async (fastify) => { // Send email to guest when manager adds a reply if (replyText) { - // Determine email: from booking (direct reviews) or manually entered (QR reviews) - let emailTo: string | null = guestEmail?.trim() || null + // Determine email: manager override → review's own guest_email (QR) → booking email + let emailTo: string | null = guestEmail?.trim() || (rows[0].guest_email as string | null) || null let guestFirstName = '' if (!emailTo && rows[0].booking_id) { @@ -329,11 +329,11 @@ const reviewsRoutes: FastifyPluginAsync = async (fastify) => { ) // ── POST /api/public/review-qr/:slug/:room ──────────────────────────────── - fastify.post( + fastify.post( '/api/public/review-qr/:slug/:room', async (request, reply) => { const { slug, room } = request.params - const { rating, text } = request.body + const { rating, text, email } = request.body if (!rating || rating < 1 || rating > 5) { return reply.code(400).send({ error: 'Rating must be 1–5' }) @@ -354,11 +354,12 @@ const reviewsRoutes: FastifyPluginAsync = async (fastify) => { await db.query( `INSERT INTO reviews - (hotel_id, booking_id, guest_id, guest_name, source, rating, text, is_public) - VALUES ($1, NULL, NULL, $2, 'qr', $3, $4, $5)`, + (hotel_id, booking_id, guest_id, guest_name, guest_email, source, rating, text, is_public) + VALUES ($1, NULL, NULL, $2, $3, 'qr', $4, $5, $6)`, [ hotel.id, roomRows[0] ? `Номер ${room}` : `QR (номер ${room})`, + email?.trim() || null, rating, text ?? null, rating >= 4, diff --git a/src/lib/api.ts b/src/lib/api.ts index afa8498..668e6e0 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1980,6 +1980,7 @@ export interface ReviewApi { bookingId: string | null guestId: string | null guestName: string + guestEmail: string | null source: string rating: number text: string | null diff --git a/src/pages/GuestReviewQrPage.tsx b/src/pages/GuestReviewQrPage.tsx index 3a5213c..f335a66 100644 --- a/src/pages/GuestReviewQrPage.tsx +++ b/src/pages/GuestReviewQrPage.tsx @@ -24,6 +24,7 @@ export function GuestReviewQrPage() { const [rating, setRating] = useState(0) const [hover, setHover] = useState(0) const [text, setText] = useState('') + const [email, setEmail] = useState('') const [submitted, setSubmitted] = useState(false) const [submitting, setSubmitting] = useState(false) @@ -45,7 +46,7 @@ export function GuestReviewQrPage() { await fetch(`${BASE}/api/public/review-qr/${slug}/${encodeURIComponent(room)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ rating, text: text.trim() || undefined }), + body: JSON.stringify({ rating, text: text.trim() || undefined, email: email.trim() || undefined }), }) setSubmitted(true) } catch { @@ -143,6 +144,16 @@ export function GuestReviewQrPage() { /> )} + {rating > 0 && ( + setEmail(e.target.value)} + /> + )} +