Major UX improvements across multiple pages

Шахматка:
- Date/period picker dropdown on navigation button (choose start date + days window)
- Cancelled bookings fade out with animation after 1 second

Бронирования:
- Clickable column headers with sort asc/desc (Гость, Заезд, Выезд, Статус, Источник, Сумма)

Страница входа:
- Removed role-based account selector — just email + password
- System auto-detects role/hotel from credentials

Настройки:
- New "Бронирование" section with room assignment strategy (spread/together/sequential/manual)
- Notifications: SMTP email config + SMS provider config (SMSC, SMS.ru, МТС, etc.)

Модули:
- Added Housekeeping and Channel Manager as proper modules
- Channel Manager lists: Booking.com, Airbnb, Яндекс Путешествия, Островок, Суточно.ру, OneTwoTrip
- Housekeeping visible to all roles (including housekeeper) via module status
- Sidebar now uses module status to show/hide Уборка and Каналы

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 15:20:05 +03:00
parent 4e615359eb
commit c233590c4b
29 changed files with 2171 additions and 149 deletions

View File

@@ -5,10 +5,10 @@ import { useAuth } from '../contexts/AuthContext'
import { useTheme } from '../contexts/ThemeContext'
import { cn } from '../lib/utils'
const DEMO_ACCOUNTS = [
{ label: 'Менеджер отеля', email: 'manager@grand-palace.ru', role: 'hotel_manager' },
{ label: 'Горничная', email: 'cleaner@grand-palace.ru', role: 'housekeeper' },
{ label: 'Супер-администратор', email: 'admin@hotelsync.io', role: 'super_admin' },
const DEMO_EMAILS = [
'manager@grand-palace.ru',
'cleaner@grand-palace.ru',
'admin@hotelsync.io',
]
export function LoginPage() {
@@ -16,8 +16,8 @@ export function LoginPage() {
const { theme, toggle } = useTheme()
const navigate = useNavigate()
const [email, setEmail] = useState('manager@grand-palace.ru')
const [password, setPassword] = useState('demo')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [showPass, setShowPass] = useState(false)
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
@@ -41,12 +41,6 @@ export function LoginPage() {
else navigate('/calendar')
}
const fillDemo = (acc: typeof DEMO_ACCOUNTS[number]) => {
setEmail(acc.email)
setPassword('demo')
setError('')
}
return (
<div className="min-h-screen bg-gradient-to-br from-brand-50 via-white to-slate-100 dark:from-slate-900 dark:via-slate-900 dark:to-brand-950 flex">
{/* Left panel — branding */}
@@ -88,7 +82,6 @@ export function LoginPage() {
{/* Right panel — login form */}
<div className="flex-1 flex flex-col">
{/* Theme toggle */}
<div className="flex justify-end p-4">
<button onClick={toggle} className="btn-ghost p-2">
{theme === 'light' ? <Moon size={18} /> : <Sun size={18} />}
@@ -114,29 +107,6 @@ export function LoginPage() {
Войдите в свой аккаунт для доступа к панели управления
</p>
{/* Demo account chips */}
<div className="mb-5">
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-2">
Демо-аккаунты:
</p>
<div className="flex flex-wrap gap-2">
{DEMO_ACCOUNTS.map(acc => (
<button
key={acc.email}
onClick={() => fillDemo(acc)}
className={cn(
'px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors',
email === acc.email
? 'bg-brand-600 text-white border-brand-600'
: 'bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 border-slate-200 dark:border-slate-600 hover:border-brand-400',
)}
>
{acc.label}
</button>
))}
</div>
</div>
{/* Form */}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
@@ -194,9 +164,28 @@ export function LoginPage() {
</button>
</form>
<p className="text-center text-xs text-slate-400 dark:text-slate-500 mt-6">
Пароль для демо: <code className="bg-slate-100 dark:bg-slate-700 px-1.5 py-0.5 rounded">demo</code>
</p>
{/* Demo hint */}
<div className="mt-6 p-3 rounded-lg bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700">
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-2">
Демо-аккаунты (пароль: <code className="bg-slate-100 dark:bg-slate-700 px-1 rounded">demo</code>):
</p>
<div className="flex flex-col gap-1">
{DEMO_EMAILS.map(e => (
<button
key={e}
onClick={() => { setEmail(e); setPassword('demo'); setError('') }}
className={cn(
'text-left text-xs px-2 py-1 rounded transition-colors',
email === e
? 'text-brand-700 dark:text-brand-300 bg-brand-50 dark:bg-brand-900/30 font-medium'
: 'text-slate-500 dark:text-slate-400 hover:text-brand-600 dark:hover:text-brand-400',
)}
>
{e}
</button>
))}
</div>
</div>
</div>
</div>
</div>