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:
@@ -8,7 +8,6 @@ import { useTheme } from '../contexts/ThemeContext'
|
||||
const SECTIONS = [
|
||||
{ id: 'general', label: 'Основные', icon: Building2 },
|
||||
{ id: 'booking', label: 'Бронирование', icon: BedDouble },
|
||||
{ id: 'guests', label: 'Гости', icon: Users },
|
||||
{ id: 'theme', label: 'Внешний вид', icon: Globe },
|
||||
{ id: 'notify', label: 'Уведомления', icon: Bell },
|
||||
{ id: 'security', label: 'Безопасность', icon: Shield },
|
||||
@@ -32,6 +31,17 @@ export function SettingsPage() {
|
||||
const hotel = MOCK_HOTELS[0]
|
||||
const { theme, toggle } = useTheme()
|
||||
|
||||
const [calendarCompact, setCalendarCompact] = useState(
|
||||
() => localStorage.getItem('calendarCompact') === 'true',
|
||||
)
|
||||
const toggleCalendarCompact = () => {
|
||||
setCalendarCompact(v => {
|
||||
const next = !v
|
||||
localStorage.setItem('calendarCompact', String(next))
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const [form, setForm] = useState({
|
||||
name: hotel.name,
|
||||
address: hotel.address,
|
||||
@@ -283,95 +293,6 @@ export function SettingsPage() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ── GUESTS ── */}
|
||||
{section === 'guests' && (
|
||||
<>
|
||||
<h2 className="font-semibold text-slate-900 dark:text-slate-100">Статусы гостей</h2>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">
|
||||
Теги отображаются при создании и редактировании бронирования
|
||||
</p>
|
||||
|
||||
{/* Tag list */}
|
||||
<div className="space-y-2">
|
||||
{guestTags.map(tag => (
|
||||
<div
|
||||
key={tag.id}
|
||||
className="flex items-center gap-3 p-3 rounded-lg bg-slate-50 dark:bg-slate-700/40"
|
||||
>
|
||||
<input
|
||||
type="color"
|
||||
value={tag.color}
|
||||
onChange={e => setGuestTags(prev => prev.map(t => t.id === tag.id ? { ...t, color: e.target.value } : t))}
|
||||
className="w-7 h-7 rounded cursor-pointer border-0 bg-transparent"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
className="input flex-1"
|
||||
value={tag.label}
|
||||
onChange={e => setGuestTags(prev => prev.map(t => t.id === tag.id ? { ...t, label: e.target.value } : t))}
|
||||
/>
|
||||
<button
|
||||
onClick={() => setGuestTags(prev => prev.filter(t => t.id !== tag.id))}
|
||||
className="p-1.5 rounded-lg text-slate-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
|
||||
>
|
||||
<XIcon size={14} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Add new tag */}
|
||||
<div className="flex items-center gap-2 pt-1">
|
||||
<input
|
||||
type="color"
|
||||
value={newTagColor}
|
||||
onChange={e => setNewTagColor(e.target.value)}
|
||||
className="w-7 h-7 rounded cursor-pointer border-0 bg-transparent shrink-0"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
className="input flex-1"
|
||||
placeholder="Название нового статуса..."
|
||||
value={newTagLabel}
|
||||
onChange={e => setNewTagLabel(e.target.value)}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter' && newTagLabel.trim()) {
|
||||
setGuestTags(prev => [...prev, { id: `tag-${Date.now()}`, label: newTagLabel.trim(), color: newTagColor }])
|
||||
setNewTagLabel('')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!newTagLabel.trim()) return
|
||||
setGuestTags(prev => [...prev, { id: `tag-${Date.now()}`, label: newTagLabel.trim(), color: newTagColor }])
|
||||
setNewTagLabel('')
|
||||
}}
|
||||
className="btn-primary"
|
||||
>
|
||||
<Plus size={14} />
|
||||
Добавить
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Preview */}
|
||||
<div className="pt-2 border-t border-slate-100 dark:border-slate-700">
|
||||
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-2">Предпросмотр тегов</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{guestTags.map(tag => (
|
||||
<span
|
||||
key={tag.id}
|
||||
className="px-2.5 py-1 rounded-lg text-xs font-medium text-white"
|
||||
style={{ backgroundColor: tag.color }}
|
||||
>
|
||||
{tag.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ── THEME ── */}
|
||||
{section === 'theme' && (
|
||||
<>
|
||||
@@ -402,6 +323,18 @@ export function SettingsPage() {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Calendar compact mode */}
|
||||
<div className="pt-3 border-t border-slate-100 dark:border-slate-700">
|
||||
<p className="text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Настройки календаря</p>
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-slate-50 dark:bg-slate-700/40">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-900 dark:text-slate-100">Компактный режим</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">Уменьшенные строки в сетке бронирований</p>
|
||||
</div>
|
||||
<Toggle on={calendarCompact} onChange={toggleCalendarCompact} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user