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:
@@ -2,7 +2,7 @@ import { useState } from 'react'
|
||||
import {
|
||||
Search, Star, Phone, Mail, User, TrendingUp, Award, Users,
|
||||
Calendar, CreditCard, X, ChevronRight, MessageSquare, Tag,
|
||||
Repeat2, ShieldCheck,
|
||||
Repeat2, ShieldCheck, Plus,
|
||||
} from 'lucide-react'
|
||||
import { cn } from '../lib/utils'
|
||||
|
||||
@@ -443,7 +443,100 @@ function GuestModal({ guest, onClose }: { guest: Guest; onClose: () => void }) {
|
||||
|
||||
// ─── Main Page ────────────────────────────────────────────────────────────────
|
||||
|
||||
// ─── Guest Statuses Tab ───────────────────────────────────────────────────────
|
||||
|
||||
function GuestStatusesTab() {
|
||||
const [tags, setTags] = useState([
|
||||
{ id: 'vip', label: 'VIP', color: '#F59E0B' },
|
||||
{ id: 'regular', label: 'Постоянный гость', color: '#3B82F6' },
|
||||
{ id: 'corp', label: 'Корпоративный', color: '#8B5CF6' },
|
||||
{ id: 'honey', label: 'Медовый месяц', color: '#EC4899' },
|
||||
{ id: 'bday', label: 'День рождения', color: '#10B981' },
|
||||
{ id: 'special', label: 'Особые пожелания', color: '#64748B' },
|
||||
{ id: 'problem', label: 'Проблемный', color: '#EF4444' },
|
||||
])
|
||||
const [newLabel, setNewLabel] = useState('')
|
||||
const [newColor, setNewColor] = useState('#4F46E5')
|
||||
|
||||
const addTag = () => {
|
||||
if (!newLabel.trim()) return
|
||||
setTags(prev => [...prev, { id: `tag-${Date.now()}`, label: newLabel.trim(), color: newColor }])
|
||||
setNewLabel('')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4 max-w-lg">
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">
|
||||
Статусы отображаются в карточках гостей и при создании бронирования. Они помогают быстро идентифицировать тип гостя.
|
||||
</p>
|
||||
|
||||
{/* Tag list */}
|
||||
<div className="card overflow-hidden divide-y divide-slate-100 dark:divide-slate-700">
|
||||
{tags.map(tag => (
|
||||
<div key={tag.id} className="flex items-center gap-3 px-4 py-3">
|
||||
<input
|
||||
type="color"
|
||||
value={tag.color}
|
||||
onChange={e => setTags(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 shrink-0"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
className="input flex-1"
|
||||
value={tag.label}
|
||||
onChange={e => setTags(prev => prev.map(t => t.id === tag.id ? { ...t, label: e.target.value } : t))}
|
||||
/>
|
||||
<span
|
||||
className="px-2.5 py-1 rounded-lg text-xs font-medium text-white shrink-0"
|
||||
style={{ backgroundColor: tag.color }}
|
||||
>
|
||||
{tag.label}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setTags(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 shrink-0"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Add new tag */}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="color"
|
||||
value={newColor}
|
||||
onChange={e => setNewColor(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={newLabel}
|
||||
onChange={e => setNewLabel(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter') addTag() }}
|
||||
/>
|
||||
<button onClick={addTag} className="btn-primary shrink-0">
|
||||
<Plus size={14} />
|
||||
Добавить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Page ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
const PAGE_TABS = [
|
||||
{ id: 'list', label: 'База гостей' },
|
||||
{ id: 'statuses', label: 'Статусы гостей' },
|
||||
] as const
|
||||
type PageTab = typeof PAGE_TABS[number]['id']
|
||||
|
||||
export function GuestsPage() {
|
||||
const [pageTab, setPageTab] = useState<PageTab>('list')
|
||||
const [search, setSearch] = useState('')
|
||||
const [filterTag, setFilterTag] = useState<GuestTag | 'all'>('all')
|
||||
const [selectedGuest, setSelectedGuest] = useState<Guest | null>(null)
|
||||
@@ -472,6 +565,28 @@ export function GuestsPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-1 border-b border-slate-200 dark:border-slate-700 -mt-2">
|
||||
{PAGE_TABS.map(t => (
|
||||
<button
|
||||
key={t.id}
|
||||
onClick={() => setPageTab(t.id)}
|
||||
className={cn(
|
||||
'px-4 py-2.5 text-sm font-medium border-b-2 -mb-px transition-colors',
|
||||
pageTab === t.id
|
||||
? 'border-brand-600 text-brand-600 dark:text-brand-400 dark:border-brand-400'
|
||||
: 'border-transparent text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-300',
|
||||
)}
|
||||
>
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{pageTab === 'statuses' && <GuestStatusesTab />}
|
||||
|
||||
{pageTab === 'list' && <>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{[
|
||||
@@ -641,6 +756,7 @@ export function GuestsPage() {
|
||||
{selectedGuest && (
|
||||
<GuestModal guest={selectedGuest} onClose={() => setSelectedGuest(null)} />
|
||||
)}
|
||||
</>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user