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 звёзд