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

@@ -41,6 +41,7 @@ interface ObjectFormData {
openHour: number
closeHour: number
maxHoursPerSlot: number | ''
bufferMinutes: number
description: string
}
@@ -48,6 +49,7 @@ const EMPTY_FORM: ObjectFormData = {
name: '', icon: '🎾', color: 'bg-green-500',
pricePerHour: 1000, pricePerDay: 5000,
openHour: 9, closeHour: 21, maxHoursPerSlot: '',
bufferMinutes: 0,
description: '',
}
@@ -65,6 +67,7 @@ function ObjectFormModal({
pricePerHour: initial.pricePerHour, pricePerDay: initial.pricePerDay,
openHour: initial.openHour, closeHour: initial.closeHour,
maxHoursPerSlot: initial.maxHoursPerSlot ?? '',
bufferMinutes: initial.bufferMinutes ?? 0,
description: '',
}
: EMPTY_FORM,
@@ -86,6 +89,7 @@ function ObjectFormModal({
openHour: form.openHour,
closeHour: form.closeHour,
maxHoursPerSlot: form.maxHoursPerSlot !== '' ? Number(form.maxHoursPerSlot) : undefined,
bufferMinutes: form.bufferMinutes > 0 ? form.bufferMinutes : undefined,
})
}
@@ -171,6 +175,31 @@ function ObjectFormModal({
</div>
</div>
{/* Buffer time */}
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
Технический перерыв между бронями
</label>
<select
className="input"
value={form.bufferMinutes}
onChange={e => set('bufferMinutes', parseInt(e.target.value))}
>
{[
{ value: 0, label: 'Без перерыва' },
{ value: 15, label: '15 минут' },
{ value: 30, label: '30 минут' },
{ value: 45, label: '45 минут' },
{ value: 60, label: '1 час' },
{ value: 90, label: '1,5 часа' },
{ value: 120, label: '2 часа' },
].map(o => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
<p className="text-xs text-slate-400 mt-1">Время для уборки СПА, подготовки корта и т.д.</p>
</div>
{/* Hours */}
<div className="grid grid-cols-3 gap-3">
<div>