379 Commits

Author SHA1 Message Date
4b337a3b85 feat: tech tasks page, priority in HK context menu, room status sync
Context menu (RoomContextMenu):
- Dirty/Cleaning now show priority sub-form (срочно/высокий/средний/низкий)
- New "Тех. задача" section with description text + priority grid

Calendar (CalendarPage):
- handleRoomUpdate: creates task for both 'dirty' AND 'cleaning' status
- New handleMaintenanceTaskCreate for category=maintenance tasks
- Passes priority from context menu to task creation

Backend (housekeeping.ts):
- task status → in_progress: room housekeeping_status = 'cleaning',
  broadcasts housekeeping_done with roomStatus='cleaning' so all
  connected Calendar clients update in real-time

DB (migration 025):
- category column on housekeeping_tasks (default 'housekeeping')
- HousekeepingPage filters category=housekeeping
- TechnicalPage filters category=maintenance

New TechnicalPage (/technical):
- List/filter/delete maintenance tasks
- Status dropdown per task
- Create modal with room/user/priority/date

Sidebar: "Тех. задачи" nav item added

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 23:35:24 +03:00
3bb7262b59 feat: add category field to housekeeping tasks + TechnicalPage for maintenance tasks
- Add migration 025 to add `category` column (housekeeping/maintenance) to housekeeping_tasks
- Backend: filter by ?category= in GET, store category in POST, allow patching category
- API client: HkPayload + housekeeping.list() now support category param
- HousekeepingPage: filters by category=housekeeping so maintenance tasks don't appear
- RoomContextMenu: priority sub-form for dirty/cleaning status, tech task creation inline form
- BookingCalendar: accepts onMaintenanceTaskCreate prop, passes priority to onRoomUpdate
- CalendarPage: handleRoomUpdate creates task for dirty+cleaning (with priority), new handleMaintenanceTaskCreate for category=maintenance tasks
- TechnicalPage: new page for viewing/creating/updating maintenance tasks
- App.tsx: /technical route added
- Sidebar: Тех. задачи nav item added under Управление

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 23:28:42 +03:00
9be51e5d07 fix: jobs run every 1h, auto-checkout from hotel check_out_time
- Interval reduced from 10m to 1h (min period is 2h — hourly is enough)
- Auto-checkout threshold: check_out date + hotel.check_out_time + N hours
  (e.g. checkout Mar 23 + 12:00 + 2h = auto-checkout at 14:00)
- Auto-cancel threshold: check_in date + hotel.check_in_time + N hours

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 23:13:55 +03:00
b9116c427a feat: notifications API, auto-jobs, calendar housekeeping fixes
Backend:
- notifications table (024 migration) — stores per-hotel notifications
- GET/PATCH/POST/DELETE /api/hotels/:slug/notifications endpoints
- createNotification() helper used by jobs + future hooks
- jobs.ts — background tasks every 10min: auto-cancel no-shows and
  auto-checkout overdue stays based on hotel_settings, broadcasts WS
  events and creates notifications for each automated action

Frontend:
- CalendarPage: handle housekeeping_done WS → update room status in
  calendar in real-time (bug fix)
- CalendarPage: when manually setting room to dirty via context menu,
  auto-create housekeeping task and broadcast via WS (bug fix)
- NotificationsContext: replaced mock data with real API integration,
  polls every 60s, syncs markRead/delete to backend
- SettingsPage booking section: auto-cancel no-show toggle + hours
  selector; auto-checkout toggle + hours selector
- api.ts: notifications API methods
- useHotelSocket: notification:new WS message type

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 23:02:30 +03:00
b631a281d2 fix: normalizeRoom converts NUMERIC baseRate string to number in api.ts
PostgreSQL NUMERIC columns are returned as strings by node-postgres.
Added normalizeRoom() that coerces baseRate/hourlyRate/floor to JS numbers
before they reach components, eliminating string concatenation bugs like
"300005000.005000.00" in price calculations.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 22:48:51 +03:00
9af11df1b0 feat: housekeeping automation — auto-task on checkout + room status on done
Backend:
- Migration 023: housekeeping_settings table (checkout_auto, checkout_priority, inspection_after_clean)
- New route: GET/PATCH /api/hotels/:slug/housekeeping-settings
- bookings.ts: on checked_out → auto-create turnover task + mark room dirty + broadcast WS
- housekeeping.ts: on task done → update room status (inspect|clean per setting) + broadcast WS
- ws.ts: export broadcast() for use in other routes

Frontend:
- HousekeepingPage: load/save settings to API, live Loader on save button
- useHotelSocket: add housekeeping WS message types
- HousekeepingPage: subscribe to WS — new tasks appear instantly without refresh
- BookingModal: show total section when room + nights selected (not just when total > 0)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 22:27:18 +03:00
ce20c6545d fix: robust night price calculation in BookingModal + clear test DB data
- Use parseISO instead of new Date() for safer date parsing
- Use Math.floor(nights) to avoid float iteration issues
- Type-check override price before using (typeof p === 'number')
- Label now shows actual sum from overrides instead of baseRate × nights

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 16:54:17 +03:00
15936753b9 feat: use availability rate overrides in calendar tooltip and booking modal
- Load rate_overrides in CalendarPage, pass as priceOverrides to BookingCalendar
- Tooltip shows override price for the hovered date (falls back to baseRate)
- BookingModal sums per-night prices from overrides for the stay period
- Room baseRate now only used as initial default for availability grid

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 16:35:13 +03:00
70531c3a9a fix: RateOverrideApi type to camelCase — transformKeys was converting keys but type was snake_case
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 16:26:51 +03:00
2a741d89d0 fix: rate overrides save + add Без категории group
- Fix applyPrices: build overrides array before setPrices (updater is async, side-effects inside it are unreliable)
- Add "Без категории" group for rooms not assigned to any category

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 16:23:10 +03:00
e5daf91ed4 feat: persist rate overrides to DB when applying prices in AvailabilityPage
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 16:14:45 +03:00
8312d2372e fix: maintenance overlay + context menu UX improvements
- Calendar overlay: normalize date strings (slice 0-10) to handle full ISO timestamps from Postgres; fix off-by-one (to = exclusive); stronger stripe visibility; guard against vFrom >= vTo
- Context menu: dateFrom defaults to today, indefinite defaults to false (both date inputs shown immediately)
- DateRangeForm: extracted DateInput component with CalendarDays icon that triggers native date picker
- HK status order: clean → inspect → cleaning → dirty

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 16:03:17 +03:00
c3a30be74e feat: calendar hover price tooltip + AvailabilityPage API integration
- Calendar cells: hold mouse 600ms to show price tooltip for that date/room (baseRate + hourly rate if enabled)
- AvailabilityPage: load real categories from API, derive basePrice from room baseRates, fall back to room types if no categories
- Rate periods: persisted to DB via new /api/hotels/:slug/rate-periods endpoint
- New backend migration 021_rate_periods.sql + rate-periods route
- Added api.ratePeriods.{list,create,update,delete} to frontend API client

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 15:05:12 +03:00
63da3d21cc feat: maintenance/blocked date ranges with booking overlap warning
- DB migration 020: add maintenance_from, maintenance_to to rooms
- Room type + RoomPayload: maintenanceFrom, maintenanceTo fields
- Context menu: clicking "На ремонт"/"Закрыт" expands inline date picker
  (С / По + "Без срока" checkbox), confirms with "Применить" button
- Calendar: stripe overlay covers only the maintenance date range
  (full row if no dates set, partial if date range specified)
- Booking creation: if drag-selected dates overlap with maintenance period,
  shows warning dialog with "Отмена" / "Всё равно забронировать"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 14:49:19 +03:00
4a39d5f731 fix: context menu reposition after mount to prevent viewport overflow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 14:41:56 +03:00
7bcc1ca0ab feat: visual overlay for maintenance/blocked rooms in calendar
- Diagonal stripe overlay on date cells: orange tint for maintenance,
  grey for blocked
- Room number and name dimmed when maintenance/blocked
- Show 🧹 dirty/cleaning indicator under room number (when not in maintenance)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 14:38:36 +03:00
fd50e02be0 feat: right-click context menu for room status + bed type capacity
- Add RoomContextMenu component (portal, Escape/click-outside to close)
  - Change room status: available / maintenance / blocked
  - Change housekeeping status: clean / dirty / cleaning / inspect
  - Open room edit (optional)
- Calendar: right-click on room label → context menu; onRoomUpdate prop
  wired to CalendarPage (calls api.rooms.update); show 🔧/🔒 badge on label
- Rooms page: right-click on any room card → same context menu;
  show orange "ремонт" badge on maintenance rooms
- BedTypesContext: add capacity field (max guests per bed type)
- BedTypes reference UI: add capacity input to add/edit form; show "N чел."

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 14:30:14 +03:00
8c6cdfb1c7 feat: configurable bed types via BedTypesContext
- Add BedTypesContext with default types + add/remove/update operations
- Replace hardcoded BED_ITEM_TYPES array in RoomModal with useBedTypes()
- Add "Справочник спальных мест" section to RoomCategoriesPage
  (expandable, same UX as amenities: add icon+label, rename, delete)
- BedItem.type changed from union to string for dynamic support

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 14:20:09 +03:00
6bf1391890 fix: "без категории" filter matches rooms with undefined categoryId
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 14:07:35 +03:00
d46101b032 fix: compact filters row with dropdowns for floor and category
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 14:04:17 +03:00
3e9622d281 feat: bulk room creation + category filter on rooms page
- Add BulkRoomModal: set number range (from→to), category, bed type,
  max guests, base rate, amenities; auto-derives floor from room number;
  preview shows new vs skipped (already existing) rooms
- Add category filter chips on Rooms page (alongside existing floor filter)
- Add "Групповое добавление" button next to "Добавить номер"
- Wire onDelete prop to RoomModal on rooms page

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 13:57:31 +03:00
eea76657e1 fix: smart next-free room number suggestion
- Placeholder shows next free after global max (e.g. 204 if 203 is max)
- Error message suggests next free starting from typed+1:
  type 101 (taken) → suggests 102
  type 201 (taken) → suggests 202
  type 1011 (taken) → suggests 1012
- Suggestion is a clickable link that auto-fills the field

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 13:51:10 +03:00
4ea6dcc368 fix: calendar room label dedup + room number validation
- BookingCalendar: remove type badge below room number (already shown in group header)
- RoomModal: validate number against existingNumbers before save
  - Red border + error message if number taken
  - Placeholder shows next free number (e.g. if 101,102 exist → placeholder 103)
  - Save button disabled while number is taken

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 13:44:33 +03:00
5d17ad0579 feat: delete photo from server on remove + fix file drag-and-drop
- DELETE /api/upload?url= endpoint removes file from disk (with path traversal protection)
- api.upload.deletePhoto() helper in frontend
- removePhoto() now calls deletePhoto for cdn.hotelsync.ru URLs (best-effort)
- handleDragOver only highlights drop zone for external file drags (not thumbnail DnD)
- <img> in preview gets draggable={false} + pointer-events-none to not interfere with drops

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 13:39:10 +03:00
036885dd24 feat: drag-and-drop photo reordering in category form
Thumbnails are draggable — drag one over another to swap positions.
Dragged item shows opacity/scale hint while dragging.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 13:28:08 +03:00
31d135563f fix: cdn.hotelsync.ru nginx config + RoomModal key for state reset
- Add cdn.hotelsync.ru server block to nginx config (serves /var/www/uploads)
- Add cdn.hotelsync.ru to HTTP redirect server_name
- RoomModal: add key={editingRoom?.id ?? 'new'} to force state reinit on room change
- RoomModal: remove categories[0] fallback for categoryId (show 'Без категории' instead)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 13:14:20 +03:00
95809851fb fix: correct token read in upload + remove upload button, keep drop zone
- api.upload.photo used wrong localStorage key ('access_token' vs 'hotelsync-session')
- Removed standalone 'Загрузить фото' button — drop zone handles click+drag
- Added '+' thumbnail button to add more photos when gallery is non-empty
- Show spinner inline during upload instead of disabled button

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 13:05:08 +03:00
c0945e7264 fix: add photos column to room_categories via migration 019
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 12:53:44 +03:00
dd75794a78 feat: organize uploads by folder + cdn.hotelsync.ru active
- Upload route now accepts ?folder=categories|rooms|hotels|guests
- Files saved to /uploads/{folder}/uuid.ext, served at cdn.hotelsync.ru/{folder}/uuid.ext
- api.upload.photo() accepts optional folder param (default: 'rooms')
- RoomCategoriesPage passes folder='categories' on photo upload
- SSL cert issued and nginx config active for cdn.hotelsync.ru

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 12:41:27 +03:00
f625d82cc5 fix: downgrade @fastify/multipart to ^8 and @fastify/static to ^7 (Fastify 4 compat)
v9 of both plugins requires Fastify 5.x — we're on 4.x

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 12:32:29 +03:00
3ed3156fe8 feat: move amenities directory to RoomCategoriesPage + cdn.hotelsync.ru for uploads
- Remove "Справочник удобств" block from RoomsPage, add to RoomCategoriesPage
- Fix RoomsPage imports (add Pencil back, remove unused amenity icons)
- Update hint in CategoryForm to point to current page
- Upload route now returns cdn.hotelsync.ru URLs instead of api.hotelsync.ru/uploads/
- Nginx config for cdn.hotelsync.ru (serves /opt/hotelsync/uploads directly)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 12:31:08 +03:00
3ef7f8265a feat: real file upload to /opt/hotelsync/uploads + serve via /uploads/ on API 2026-03-23 12:23:13 +03:00
5540d0cdfa fix: category form — async save with error display, drag-and-drop photos, 20MB body limit 2026-03-23 12:17:53 +03:00
1f761e65cf feat: room category — remove hardcoded types, show 'Без категории' with link when none exist 2026-03-23 12:08:47 +03:00
04258a4f50 feat: room type dropdown uses categories from API when available 2026-03-23 12:02:32 +03:00
c36d94e22a feat: show occupied slots in new rental step-1 modal 2026-03-23 11:54:52 +03:00
d54ea551b4 fix: RoomsPage load categories from API instead of mock 2026-03-23 11:45:40 +03:00
5c40b154d0 feat: categories + tariffs — migration, API routes, frontend integration 2026-03-23 11:42:29 +03:00
b494224af0 feat: BookingsPage rental tab — connect to API (replace mock data) 2026-03-23 11:19:04 +03:00
2475c76d45 feat: RentalBookingModal 2-column layout
Left col: existing bookings list, full-day toggle, time selectors
Right col: guest autocomplete, phone, link-to-room, notes, price+payment block
Modal widened to max-w-2xl; payment section unified with BookingModal style

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 01:38:25 +03:00
7c65bebc22 feat: rental tab 2-column layout + payment section
Left col: object picker, date, full-day toggle, time selectors
Right col: guest autocomplete, phone, notes, total + payment block
Payment: Наличные/Терминал toggle buttons, оплачено input, 'Всю сумму' shortcut, debt indicator; paidAmount saved to booking

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 01:31:16 +03:00
b1b6854663 feat: single ФИО field with autocomplete replaces 3 separate name inputs
- BookingModal (room tab): replace Фамилия/Имя/Отчество grid with single 'ФИО *' input; parseFullName() splits by spaces into last/first/middle; DB search via api.guests.list (debounced 300ms); dropdown shows current bookings + DB guests with phone/email autofill
- BookingDetailPanel: merge Фамилия (with autocomplete) + Имя + Отчество into single ФИО * input; existing acQuery/acResults infrastructure reused; parseFio() updates all 3 bgForm fields on every keystroke; openEditForm initializes ФИО from full name
- RentalBookingModal + BookingModal rental tab: include middleName when displaying DB guest results and when setting guestName on selection; update type to include middleName field

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 01:22:46 +03:00
e403689522 fix: toggle CSS + guest autocomplete in BookingModal rental tab
- Toggle: add type=button, overflow-hidden, p-0, explicit left-[2px] + translate-x-[18px] pattern for reliable knob positioning across all toggles
- Guest field in BookingModal rental tab: add debounced DB search (api.guests.list) + suggestions dropdown (same as RentalBookingModal)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 01:08:25 +03:00
f21c5d5054 fix: rental calendar cells cap at 3 bookings + show +N overflow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 23:08:25 +03:00
db05898c08 feat: 15-minute rental slots + compact calendar cell display
- Rental bookings now use minutes since midnight (start_hour/end_hour
  store e.g. 495 for 8:15, 570 for 9:30) — DB migration 017
- Buffer applied in exact minutes (no hour ceiling): 15min buffer after
  9:00 booking → next slot available at 9:15
- Calendar cell: removed working-hours line (8:00–22:00), now shows
  colored dot + compact time per booking (8–10, 8:15–9:15)
- Time selects show HH:MM format for 15-min intervals

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 23:02:24 +03:00
370fe8a9a9 feat: move rental module to main nav under Цены и тарифы
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 22:46:39 +03:00
20582581be fix: sync endHour state when effectiveStartHour shifts past it
When existing bookings block the initial startHour, effectiveStartHour
snaps to the first available slot. Added useLayoutEffect to update
endHour state immediately so start and end times are never equal.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 22:37:49 +03:00
fe4b9aa6eb feat: connect RentalPage to API (objects + bookings persisted in DB)
- Add createObject/updateObject/deleteObject to api.rental
- Add RentalObjectPayload interface
- RentalPage now loads objects and bookings from API on mount
- Save/delete object operations call the API instead of only local state
- Save booking operations call the API (create or update)
- bufferMinutes from DB is now correctly passed to booking modal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 22:23:16 +03:00
3961585196 fix: rental booking buffer applied to end times + phone mask
- getMaxEndHour now subtracts breakHours from next booking's start,
  so you can't book right up to a slot that needs buffer time before it
- effectiveStartHour moved before availableEndHours so equal start/end
  times are no longer possible
- maskPhone (+7 (XXX) XXX-XX-XX) applied to phone field in RentalBookingModal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 22:16:12 +03:00
31858ca5cc feat: room/rental tabs in booking modal + smart time slot filtering
BookingModal:
- Add 'Номер' / 'Аренда объекта' tabs when rental objects exist
- Rental tab: object picker, date, full-day toggle, conflict-aware time dropdowns, guest/phone fields, total amount

RentalBookingModal:
- Filter start times: exclude hours inside booked slots + bufferMinutes gap
- Filter end times: cap at next booking's start hour
- Disable 'Весь день' if any timed bookings exist for the day
- Show 'Все слоты заняты' when no start hours are available
- Show buffer duration in time section label

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 22:07:40 +03:00