- ReviewsPage: logo upload (circle/square), header bg image, show-email toggle
- GuestReviewQrPage/GuestReviewPage: render logo image, header bg image, logo shape
- Backend: return review_logo_url, review_logo_shape, review_header_image_url, show_email from both public review endpoints
- jobs.ts: run unread messages job 20s after startup (was only on interval)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add notificationSettings JSONB to role_permissions (migration 089)
- sendPushForNotification() — pushes only to users with the notif type enabled
- reviews.ts — push + in-app on new direct and QR reviews
- room-service.ts — use sendPushForNotification('room_service_order')
- publicWidget.ts — push + in-app on new online booking
- jobs.ts — runUnreadMessagesJob() every 15 min, deduped by in-process set
- UsersPage: NOTIFICATION_GROUPS UI in roles tab with per-type toggles
- api.ts / RolePermissionsContext: notificationSettings in types and save()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Migrations 086-088: room_service_settings, menu_items, orders+order_items
- Backend: /api/hotels/:slug/room-service/* routes (settings, menu CRUD,
orders CRUD, public config, public order status)
- push.ts: sendPushToHotel() — push to all hotel staff on new order
- On new order: in-app notification + push to hotel staff
- api.ts: RoomService types + roomService API methods
- RoomServicePage: replaced mocked state with real API, 30s polling,
add/edit/delete menu items modal, real advance/cancel order
- GuestRoomServicePage: fetches real menu + config, uses hotel payment
methods, submits real order, polls status every 15s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Settings tab: replace static source map rows with editable table
(add/delete mappings, persisted as review_source_map in hotel_settings)
- QR tab: add custom location QR codes (Ресепшн, Ресторан, etc.)
not tied to a room, stored as review_custom_locations
- Backend: GET /api/public/review/:token reads review_source_map from
hotel_settings instead of hardcoded SOURCE_KEYWORD mapping
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PATCH reviews/:id now sends email to guest when manager adds reply
(sendReviewReplyEmail: hotel-branded, shows original rating, manager's text)
- New public endpoints: GET/POST /api/public/review-qr/:slug/:room
(anonymous QR review, no token/booking required, source='qr')
- New page GuestReviewQrPage at /review-qr/:slug/:room
- ReviewsPage: new QR-коды tab — grid of QR codes per room with download SVG + print
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Email no longer shows HotelSync branding, shows hotel name instead
- Greeting uses second word of guest_name as first name (Фамилия Имя format)
- Footer link changed to hotelsync.ru
- Brand color from hotel settings applied to email button/header
- Job: only process bookings checked out within last 30 days (prevents bulk send on first activation)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace useParams with useAuth().user.hotelSlug in ReviewsPage
- Backend: filter platforms by booking source when review_redirect_source=true
(booking_com→Booking.com, yandex_travel→Яндекс, airbnb, expedia, vrbo)
- If source has matching platform — show only that one; otherwise all platforms
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- /api/auth/refresh now fetches current role from DB instead of using
stale Redis payload — role changes now take effect on next page refresh
without requiring re-login. Also deactivates token if user.active=false.
- Removed role-count stat cards from staff page (user request)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- POST /api/hotels/:slug/users: password now optional
- with password → create active user (email_confirmed=true)
- without password → create inactive user, send invite email with 7-day token
- POST /api/auth/accept-invite: validates token, sets password, activates
account, returns JWT for auto-login
- Migration 083: invite_token + invite_expires columns on users
- email.ts: sendInviteEmail() with branded HTML template
- AcceptInvitePage at /invite/:token — set password form, auto-login on success
- AuthContext: loginWithToken() for programmatic session set
- UserModal: password field optional for new users, hint about invite email
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend was returning raw Postgres snake_case column names; frontend
SavedRolePermission interface expected camelCase. This caused isSystem
to always be undefined → all roles treated as custom in UserModal.
Also adds home_page to the SELECT so homePage is available.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- createRole() now calls api.rolePermissions.save() on creation so the
role survives a page refresh (was only in local state before)
- UserModal now renders custom roles from savedPermissions context
alongside the system role buttons
- mapRole() passes custom_* role keys through unchanged
- handleSave falls back to u.role for custom roles (no backendRoleMap entry)
- Table badge handles missing ROLE_META entry for custom roles
- DB migration 082: removes users.role CHECK constraint so custom role
keys can be stored in the users table
- Backend POST/PATCH: allow custom_* roles through role allowlist checks
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>