diff --git a/backend/src/jobs.ts b/backend/src/jobs.ts
index ee4d97a..c4929c6 100644
--- a/backend/src/jobs.ts
+++ b/backend/src/jobs.ts
@@ -399,6 +399,10 @@ export function startJobs(): void {
runPaymentExpiryJob().catch(console.error)
}, PAYMENT_EXPIRY_INTERVAL)
+ setTimeout(() => {
+ runUnreadMessagesJob().catch(console.error)
+ }, 20_000)
+
setInterval(() => {
runUnreadMessagesJob().catch(console.error)
}, UNREAD_MSG_INTERVAL)
diff --git a/backend/src/routes/reviews.ts b/backend/src/routes/reviews.ts
index 723a175..2e8a255 100644
--- a/backend/src/routes/reviews.ts
+++ b/backend/src/routes/reviews.ts
@@ -170,7 +170,9 @@ const reviewsRoutes: FastifyPluginAsync = async (fastify) => {
'review_redirect_threshold','review_platforms',
'review_show_text_field','review_welcome_text',
'review_brand_color','review_redirect_source',
- 'review_source_map'
+ 'review_source_map','review_logo_url',
+ 'review_logo_shape','review_header_image_url',
+ 'review_show_email'
)`,
[row.hotel_id],
)
@@ -204,10 +206,14 @@ const reviewsRoutes: FastifyPluginAsync = async (fastify) => {
return {
hotelName: row.hotel_name,
logoLetter: (row.hotel_name as string)[0] ?? 'О',
+ logoUrl: (s.review_logo_url as string | undefined) ?? null,
+ logoShape: (s.review_logo_shape as string | undefined) ?? 'circle',
+ headerImageUrl: (s.review_header_image_url as string | undefined) ?? null,
welcomeText: (s.review_welcome_text as string | undefined) ?? 'Как вам у нас?',
color: (s.review_brand_color as string | undefined) ?? '#2563eb',
redirectThreshold: Number(s.review_redirect_threshold ?? 4),
showTextField: s.review_show_text_field !== false,
+ showEmail: s.review_show_email !== false,
platforms,
guestName: row.guest_name ?? '',
bookingSource: bookingSrc,
@@ -302,7 +308,9 @@ const reviewsRoutes: FastifyPluginAsync = async (fastify) => {
AND key IN (
'review_redirect_threshold','review_platforms',
'review_show_text_field','review_welcome_text',
- 'review_brand_color'
+ 'review_brand_color','review_logo_url',
+ 'review_logo_shape','review_header_image_url',
+ 'review_show_email'
)`,
[hotel.id],
)
@@ -318,10 +326,14 @@ const reviewsRoutes: FastifyPluginAsync = async (fastify) => {
return {
hotelName: hotel.name,
logoLetter: hotel.name[0] ?? 'О',
+ logoUrl: (s.review_logo_url as string | undefined) ?? null,
+ logoShape: (s.review_logo_shape as string | undefined) ?? 'circle',
+ headerImageUrl: (s.review_header_image_url as string | undefined) ?? null,
welcomeText: (s.review_welcome_text as string | undefined) ?? 'Как вам у нас?',
color: (s.review_brand_color as string | undefined) ?? '#2563eb',
redirectThreshold: Number(s.review_redirect_threshold ?? 4),
showTextField: s.review_show_text_field !== false,
+ showEmail: s.review_show_email !== false,
platforms: allPlatforms,
roomNumber: room,
}
diff --git a/src/lib/api.ts b/src/lib/api.ts
index 668e6e0..3b0e003 100644
--- a/src/lib/api.ts
+++ b/src/lib/api.ts
@@ -1997,10 +1997,14 @@ export interface ReviewApi {
export interface PublicReviewConfig {
hotelName: string
logoLetter: string
+ logoUrl: string | null
+ logoShape: 'circle' | 'square'
+ headerImageUrl: string | null
welcomeText: string
color: string
redirectThreshold: number
showTextField: boolean
+ showEmail: boolean
platforms: { name: string; url: string }[]
guestName: string
bookingSource: string
diff --git a/src/pages/GuestReviewPage.tsx b/src/pages/GuestReviewPage.tsx
index 88cfd7e..1ba3ff8 100644
--- a/src/pages/GuestReviewPage.tsx
+++ b/src/pages/GuestReviewPage.tsx
@@ -88,16 +88,29 @@ export function GuestReviewPage() {
-
-
-
- {/* Logo */}
+ {/* Header area */}
+
+ {config?.headerImageUrl &&
}
- {config?.logoLetter ?? 'О'}
+ {config?.logoUrl
+ ?

+ : (config?.logoLetter ?? 'О')
+ }
+
+
{config?.hotelName}
diff --git a/src/pages/GuestReviewQrPage.tsx b/src/pages/GuestReviewQrPage.tsx
index f335a66..7f81245 100644
--- a/src/pages/GuestReviewQrPage.tsx
+++ b/src/pages/GuestReviewQrPage.tsx
@@ -8,10 +8,14 @@ const BASE = (import.meta.env.VITE_API_URL as string | undefined) ?? 'https://ap
interface QrConfig {
hotelName: string
logoLetter: string
+ logoUrl: string | null
+ logoShape: 'circle' | 'square'
+ headerImageUrl: string | null
welcomeText: string
color: string
redirectThreshold: number
showTextField: boolean
+ showEmail: boolean
platforms: { name: string; url: string }[]
roomNumber: string
}
@@ -84,16 +88,29 @@ export function GuestReviewQrPage() {
-
-
-
- {/* Logo */}
+ {/* Header area */}
+
+ {config?.headerImageUrl &&
}
- {config?.logoLetter ?? 'О'}
+ {config?.logoUrl
+ ?

+ : (config?.logoLetter ?? 'О')
+ }
+
+
{config?.hotelName}
@@ -144,7 +161,7 @@ export function GuestReviewQrPage() {
/>
)}
- {rating > 0 && (
+ {rating > 0 && config?.showEmail !== false && (
('circle')
+ const [previewHeaderImageUrl, setPreviewHeaderImageUrl] = useState('')
+ const [previewShowEmail, setPreviewShowEmail] = useState(true)
+ const [uploadingLogo, setUploadingLogo] = useState(false)
+ const [uploadingHeader, setUploadingHeader] = useState(false)
// ── Load data ───────────────────────────────────────────────────────────────
@@ -144,6 +151,10 @@ export function ReviewsPage() {
if (s.review_welcome_text !== undefined) setPreviewWelcome(String(s.review_welcome_text))
if (s.review_brand_color !== undefined) setPreviewColor(String(s.review_brand_color))
if (s.review_show_text_field !== undefined) setPreviewShowText(Boolean(s.review_show_text_field))
+ if (s.review_logo_url !== undefined) setPreviewLogoUrl(String(s.review_logo_url ?? ''))
+ if (s.review_logo_shape !== undefined) setPreviewLogoShape(s.review_logo_shape as 'circle' | 'square')
+ if (s.review_header_image_url !== undefined) setPreviewHeaderImageUrl(String(s.review_header_image_url ?? ''))
+ if (s.review_show_email !== undefined) setPreviewShowEmail(s.review_show_email !== false)
if (Array.isArray(s.review_platforms)) setPlatforms(s.review_platforms as ReviewPlatform[])
if (Array.isArray(s.review_source_map)) setSourceMap(s.review_source_map as SourceMapEntry[])
if (Array.isArray(s.review_custom_locations)) setCustomLocations(s.review_custom_locations as string[])
@@ -179,6 +190,10 @@ export function ReviewsPage() {
review_welcome_text: previewWelcome,
review_brand_color: previewColor,
review_show_text_field: previewShowText,
+ review_logo_url: previewLogoUrl || null,
+ review_logo_shape: previewLogoShape,
+ review_header_image_url: previewHeaderImageUrl || null,
+ review_show_email: previewShowEmail,
review_platforms: platforms,
review_source_map: sourceMap,
review_custom_locations: customLocations,
@@ -330,13 +345,29 @@ export function ReviewsPage() {
-
+ {/* Header with optional background image */}
+
+ {previewHeaderImageUrl &&
}
- {previewHotelName[0]}
+ {previewLogoUrl
+ ?

+ : previewHotelName[0]
+ }
+
+
{previewHotelName}
{previewWelcome}
@@ -442,6 +473,63 @@ export function ReviewsPage() {
setPreviewColor(e.target.value)} title="Свой цвет" />
+ {/* Logo upload */}
+
+
Логотип
+
+ {previewLogoUrl && (
+

+ )}
+
+ {previewLogoUrl && (
+
+ )}
+
+
+ {(['circle', 'square'] as const).map(shape => (
+
+ ))}
+
+
+
+ {/* Header background image */}
+
+
Фон за логотипом
+ {previewHeaderImageUrl && (
+

+ )}
+
+
+ {previewHeaderImageUrl && (
+
+ )}
+
+
+
Показывать поле для текста
@@ -454,6 +542,18 @@ export function ReviewsPage() {
+
+
+
Поле Email для QR-отзывов
+
Гость может указать email для получения ответа
+
+
+
Текущий порог: {threshold} из 5 звёзд