- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- Widget white page fix: fallback photo when real rooms have no photos array
- Long-stay deposits: if booking > threshold days and toggle on → full charge instead of hold (bypasses YooKassa 7-day limit)
- DepositSettingsPage: toggle + day threshold input for long-stay full payment
- Refund endpoint: POST /deposit/refund supports full and partial refunds via YooKassa refunds API
- DepositWidget: refund UI for yookassa_charge deposits (full/partial, shows remaining amount)
- partially_refunded status support with badge and "Вернуть ещё" button
- Auto-confirm: webhook now auto-confirms booking status on payment.succeeded for widget bookings
- PaymentSettingsPage: auto-confirm toggle per gateway (shown when booking-widget module active)
- Migration 067: long_stay columns on hotel_deposit_settings, auto_confirm_on_payment on gateways, refunded_amount on deposits
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add releaseRequiresCheckout toggle in DepositSettingsPage (UI + payload)
- Migration 066: release_requires_checkout column on hotel_deposit_settings
- Migration 065: hotel_payment_gateways table for centralized YooKassa config
- Block deposit release buttons when releaseRequiresCheckout && not checked_out
- Fix billing calculation: use Number() to prevent PostgreSQL NUMERIC string concat
- Fix minibar billing: show current price (JOIN minibar_items.price) not recorded price
- Auto-poll deposit status every 5s while hold_created; clear QR message on confirmation
- Add billing/folio tab in BookingDetailPanel (room charges + minibar + payments + balance)
- PaymentSettingsPage: new "Онлайн-оплата (ЮКасса)" section with gateway CRUD
- DepositSettingsPage: replace YooKassa fields with link to centralized payment settings
- BookingWidgetPage: connect to real API, gateway status banner, async booking submission
- New backend routes: paymentGateways, publicWidget
- YooKassa service: add createCharge() for immediate payment (online bookings)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>