- Add web-push VAPID backend with push_subscriptions table (migration 078)
- Backend push module with sendPushToUser/sendPushToRoom helpers
- Push routes: GET vapid-key, POST subscribe, DELETE unsubscribe
- Trigger push on new chat messages in direct/group rooms
- PWA manifest, service worker, and icons (72–512px)
- Frontend push lib: SW registration, subscribe/unsubscribe helpers
- Push API in api.ts, SW registered in main.tsx
- usePushSubscription hook for auto-subscribe on permission grant
- PushPermissionRow component in ChatWidget settings view
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. Role tooltip is now absolute-positioned (no layout impact on bubble width)
2. Outside-click uses overlay div instead of document mousedown (fixes mention dropdown closing chat)
3. Header: role shown in small white/50 text, separate from green 'в сети'
4. @mentions: highlighted in messages (yellow for self, bold for others); bypass mute if mentioned
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- MessageBubble: role hidden by default, appears on hover of sender name (group/name CSS)
- Direct chat header: shows other user's role (from otherUserRole field added to backend)
- @mention: typing @ triggers filtered user dropdown, click inserts @Name into message
- Hint text updated: '@ — упомянуть'
- Backend: other_user_role added to rooms query
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Backend: cursor-based pagination via ?before=<ISO timestamp>
- Frontend: auto-load when scrolling to top (<60px), spinner + manual 'Загрузить ещё' button
- Poll merge: preserves older history when new messages arrive from polling
- hasMoreMsgs flag: shown only when exactly 50 msgs returned (more may exist)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Scroll: only auto-scroll on poll when near bottom; openRoom uses double-RAF for instant jump
- Sender role: show position (Менеджер / Горничная etc) under name in message bubbles
- Close on outside click: mousedown listener when panel is open
- Per-room mute: ⋮ menu → Отключить/Включить уведомления, BellOff badge on muted rooms, saved to localStorage
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- openRoom: set instantScrollRef before setMessages → scrollTop=scrollHeight synchronously, no visible jump
- In-room search: 🔍 button in messages header, filter messages client-side, empty state
- Room search state reset when opening/closing room
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Room poll now runs always (10s when closed, 5s when open) — badge + sound work when minimized
- Toast pop-up (top-right) on new messages when chat is minimized or room not active
- Auto-dismiss toasts after 4.5s, click to open room
- Settings: popup enable/disable + show/hide message text in notification
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- RoomRow: ⋮ button (hover) for pin/unpin context menu
- MessageBubble: right-click context menu with emoji reactions + edit/delete
- Typing indicator: debounced setTyping, animated TypingDots, polling getTyping every 2s
- Online presence: heartbeat setPresence every 30s, green dot on avatars/header
- Read receipts: ✓/✓✓ for own messages in direct rooms via otherUserLastRead
- Migration 075: chat_presence + chat_typing tables
- Context menus clamped to viewport bounds
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace hover toolbar with right-click context menu (no overflow issues)
Messages: emoji reactions row + Edit + Delete (own only)
Rooms: Pin / Unpin — pinned rooms always appear at top with 📌 icon
- Context menu clamped to viewport bounds, closes on any outside click
- Sound notifications: Web Audio API beep (880Hz, 0.25s) on new messages
and on new unreads when widget is closed; respects soundEnabled setting
- Settings: added px-4 to sections so toggle is never clipped
Added hint text about right-click shortcuts
- Pin state stored in localStorage (hotelsync-chat-pins)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Migration 073: attachment_url column in chat_messages
- upload.ts: add 'chat' folder to ALLOWED_FOLDERS
- chat.ts: accept attachment_url in send message, return it in GET messages
- api.ts: attachmentUrl in ChatMessage type, uploadImage() with raw FormData fetch
- ChatWidget: paperclip button, file preview thumbnail with remove, image display
inline in message bubbles (clickable, opens original), send enabled with image only
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Migration 072: widen type column to VARCHAR(20), add partial unique index for general rooms
- Backend: replace brittle ON CONFLICT with explicit SELECT-then-INSERT ensureRoom helper
- Frontend: show room skeletons (Общий чат + Уведомления) during load instead of spinner,
hide '0 чатов' subtitle when list is empty
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Standalone page: ?width=full removes max-w-lg container constraint
- iframeSnippet: passes &width=full to URL when 100% width selected
- public/widget-loader.js: hosted script, reads data-* attrs, no inline code needed
- data-mode="floating" → styled FAB button bottom-right
- data-mode="trigger" → listens for .hotelsync-book class clicks
- Proper modal with close button and backdrop click
- Code tab snippets for floating/trigger are now single <script src> lines
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- displayRooms now maps r.photos (URL strings) to photoUrls[]
- Collapsed thumbnail: shows <img> if real photo URL exists, falls back to emoji
- Expanded gallery: full-width <img> for real photos, gradient for mock
- Thumbnail strip: renders real photo thumbnails when available
- Photo nav buttons use totalPhotos (real or mock count)
- beds/area/rating hidden when value is 0 (real rooms have no beds/area/rating data)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Additional services in widget are now derived from rental_objects (not a separate list)
- Settings page shows rental objects with enable/disable toggle; link to rental module
- Saves only {id, enabled}[] to widget_services — rental objects manage name/price/icon
- Standalone widget also derives additionalServices from config.rentalObjects
- Widget config API includes services[] in widgetSettings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Cards now show wide photo (h-44) at top instead of small thumbnail
- Gallery navigation: left/right arrows on hover, dot indicators, counter
- Gradient overlay with available badge (top-left) and photo count
- Select button at bottom of each card
- Graceful fallback to 🛏️ placeholder when no photos
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Services can now be added, edited inline (icon/name/price/unit), and deleted
- List persisted to DB as widget_services key in hotel_settings (auto-save)
- Services loaded from DB on settings page init; fall back to DEFAULT_SERVICES
- Toggle enable/disable retained; edit/delete buttons appear on row hover
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Category cards now show photos[0] as <img> when available, fallback to 🛏️ emoji
- Settings page preview loads rentalObjects from API and passes to WidgetPreview
- MockCategory type and MOCK_CATEGORIES get photos: [] to match updated type
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add upsertGuestFromBooking() service — creates or updates guest record
when a booking is made (matches by email/phone, creates if not found)
- Integrated into bookings.ts (manual PMS bookings) and publicWidget.ts
(online widget bookings)
- Name parsed as Russian format: "Фамилия Имя [Отчество]"
- Non-throwing: booking creation never fails due to guest upsert errors
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add rental_objects to /api/widget/:slug/config response
- Add POST /api/widget/:slug/rental-bookings endpoint with availability check
- Widget rental tab now shows real rental objects (not additionalServices)
- Date picker, hourly or full-day toggle, start/end hour selectors
- Booking submit creates actual rental_booking record in DB
- Success screen shows rental object, date and time slot
- WidgetRentalObject + WidgetRentalBookingPayload types added to api.ts
- BookingWidgetStandalonePage passes rentalObjects to WidgetPreview
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Payment tab shows 'Оплачено' from booking_payments table, not from paid_amount.
Webhook now inserts a payment record so the tab shows the correct amount.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix date display: slice ISO timestamp to first 10 chars before splitting
- Add sendBookingConfirmedEmail() to email.ts — sends green styled HTML email
with booking details (dates, amount) on payment.succeeded webhook
- Webhook now sends confirmation email to guest email if present
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add YooKassa webhook handler (POST /api/yookassa/webhook) that updates
booking to confirmed+paid on payment.succeeded, cancelled on payment.canceled
- Add public status endpoint GET /api/online-bookings/:id for return URL polling
- Add 'reserved' booking status (violet) shown in calendar while awaiting payment
- Add 'website' to BookingSource type
- Payment timeout job (every 2 min) auto-cancels expired unpaid bookings
- Widget setting: "Время ожидания оплаты" (5–60 min, default 15)
- BookingConfirmPage at /booking-confirm/:id — polls status, countdown timer,
shows success/pending/cancelled state
- Widget bookings now use status='reserved' instead of 'inquiry' when YooKassa
is configured; set to 'confirmed' by webhook on payment.succeeded
- Migration 070: add 'reserved' to bookings status constraint + payment_expires_at
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- localDate() uses local timezone instead of UTC (prevents showing yesterday
after midnight in UTC+3)
- Guest lookup: fallback to bookings table by guest_email/guest_phone when
no guest profile found in CRM (guests table)
- Fix REGEXP_REPLACE pattern from '\D' (PCRE) to '[^0-9]' (PostgreSQL POSIX)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Widget bookings use source='website' which violated the existing
constraint. Migration 069 expands the constraint to include 'website'.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- On API error: show error message instead of false success screen
- Success message: "Оплата прошла успешно" only when paidViaGateway=true (user actually clicked pay in YooKassa); otherwise "Бронирование принято. Менеджер свяжется с вами" or "Оплата на месте"
- Payment screen: simplified to YooKassa-only redirect (removed bank transfer variant)
- Removed bankDetails from WidgetSettings entirely (not needed per product decision)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Always show payment step when paymentEnabled=true even if YooKassa
confirmationUrl is null (payment creation failed silently)
- Add third payment screen variant: "manager will contact you" when no
YooKassa URL and no bank details configured
- Add Russian phone mask +7 (___) ___-__-__ on phone type fields
- Default check-in/check-out dates to today/tomorrow instead of hardcoded past dates
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add is_blacklisted column to guests (migration 068)
- POST /widget/bookings: block blacklisted guests (403, generic message)
- GET /widget/:slug/guests/lookup: find existing guest by email/phone for auto-fill
- Widget form: debounce guest lookup on email/phone input, show "Заполнить" suggestion
- Widget payment screen: replace fake card form with proper YooKassa redirect screen or bank transfer details page
- Add bank transfer details field to widget settings (saved as widget_bank_details)
- GuestsPage: blacklist toggle button in edit modal + 🚫 ЧС badge in table
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BookingWidgetStandalonePage now fetches widgetSettings from the public
config API and applies them (color, language, mode, etc.) instead of
always falling back to URL param defaults. URL params still work as
overrides if present.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>