Add role permissions, loyalty accrual rules, guest statuses, and room service guest page

- UsersPage: add "Роли и права" tab with per-module permission editor per role; create/delete custom roles
- LoyaltyPage: add "Правила начисления" tab with togglable accrual rules editor; add "Ручное начисление" tab with guest search by name/phone/email, add/deduct points with reason and history; fix banner text (HotelSync is a PMS, not a hotel chain)
- GuestsPage: add "Статусы гостей" tab (moved from Settings)
- SettingsPage: remove Guests section; add compact calendar toggle in Appearance; toggle persists via localStorage
- BookingCalendar: read compact mode default from localStorage
- App.tsx: add public route /room-service/:slug → GuestRoomServicePage
- GuestRoomServicePage: new public guest-facing page for ordering room service with menu, cart, payment, and order status tracking
- RoomServicePage: QR/link sharing, payment mode toggles, new order notifications
- RentalPage/rentalData: buffer time between bookings (bufferMinutes field)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-16 20:13:22 +03:00
parent a4e484cc0d
commit d661bbbc84
10 changed files with 2002 additions and 487 deletions

View File

@@ -74,8 +74,10 @@ export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpd
const [dragStart, setDragStart] = useState<{ roomId: string; dayIdx: number } | null>(null)
const [dragEnd, setDragEnd] = useState<number | null>(null)
// Compact mode
const [compact, setCompact] = useState(false)
// Compact mode (default from Settings → Appearance)
const [compact, setCompact] = useState(
() => localStorage.getItem('calendarCompact') === 'true',
)
const rowHeight = compact ? ROW_HEIGHT_COMPACT : ROW_HEIGHT
// Modals
@@ -253,7 +255,7 @@ export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpd
</div>
<button
onClick={() => setCompact(v => !v)}
onClick={() => setCompact(v => { const next = !v; localStorage.setItem('calendarCompact', String(next)); return next })}
title={compact ? 'Обычный вид' : 'Компактный вид'}
className={cn(
'btn-ghost flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium',