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:
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { Search, Filter, Plus, ArrowUpDown } from 'lucide-react'
|
||||
import { useState, useMemo } from 'react'
|
||||
import { Search, Plus, ArrowUp, ArrowDown, ArrowUpDown } from 'lucide-react'
|
||||
import { MOCK_BOOKINGS, MOCK_ROOMS } from '../data/mockData'
|
||||
import type { Booking, BookingStatus } from '../types'
|
||||
import { cn, BOOKING_STATUS_BADGE, BOOKING_STATUS_LABELS, SOURCE_COLORS, SOURCE_LABELS, formatCurrency, nightsCount } from '../lib/utils'
|
||||
@@ -17,12 +17,32 @@ const STATUS_FILTERS: { label: string; value: BookingStatus | 'all' }[] = [
|
||||
{ label: 'Отменены', value: 'cancelled' },
|
||||
]
|
||||
|
||||
type SortKey = 'guestName' | 'checkIn' | 'checkOut' | 'status' | 'source' | 'totalAmount'
|
||||
|
||||
const COLUMNS: { key: SortKey | null; label: string }[] = [
|
||||
{ key: 'guestName', label: 'Гость' },
|
||||
{ key: null, label: 'Номер' },
|
||||
{ key: 'checkIn', label: 'Заезд' },
|
||||
{ key: 'checkOut', label: 'Выезд' },
|
||||
{ key: 'status', label: 'Статус' },
|
||||
{ key: 'source', label: 'Источник' },
|
||||
{ key: 'totalAmount', label: 'Сумма' },
|
||||
{ key: null, label: '' },
|
||||
]
|
||||
|
||||
export function BookingsPage() {
|
||||
const [bookings, setBookings] = useState<Booking[]>(MOCK_BOOKINGS)
|
||||
const [search, setSearch] = useState('')
|
||||
const [statusFilter, setStatusFilter] = useState<BookingStatus | 'all'>('all')
|
||||
const [selected, setSelected] = useState<Booking | null>(null)
|
||||
const [showCreateModal, setShowCreateModal] = useState(false)
|
||||
const [sortKey, setSortKey] = useState<SortKey | null>(null)
|
||||
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc')
|
||||
|
||||
const handleSort = (key: SortKey) => {
|
||||
if (sortKey === key) setSortDir(d => d === 'asc' ? 'desc' : 'asc')
|
||||
else { setSortKey(key); setSortDir('asc') }
|
||||
}
|
||||
|
||||
const filtered = bookings.filter(b => {
|
||||
const matchSearch = search === '' ||
|
||||
@@ -33,6 +53,21 @@ export function BookingsPage() {
|
||||
return matchSearch && matchStatus
|
||||
})
|
||||
|
||||
const sorted = useMemo(() => {
|
||||
if (!sortKey) return filtered
|
||||
return [...filtered].sort((a, b) => {
|
||||
const av = a[sortKey]
|
||||
const bv = b[sortKey]
|
||||
let cmp = 0
|
||||
if (typeof av === 'string' && typeof bv === 'string') {
|
||||
cmp = av.localeCompare(bv, 'ru')
|
||||
} else {
|
||||
cmp = (av as number) - (bv as number)
|
||||
}
|
||||
return sortDir === 'asc' ? cmp : -cmp
|
||||
})
|
||||
}, [filtered, sortKey, sortDir])
|
||||
|
||||
const room = (id: string) => MOCK_ROOMS.find(r => r.id === id)
|
||||
|
||||
return (
|
||||
@@ -41,7 +76,7 @@ export function BookingsPage() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-slate-900 dark:text-slate-100">Бронирования</h1>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">{filtered.length} из {bookings.length}</p>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">{sorted.length} из {bookings.length}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
@@ -88,15 +123,31 @@ export function BookingsPage() {
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-700/40">
|
||||
{['Гость', 'Номер', 'Заезд', 'Выезд', 'Статус', 'Источник', 'Сумма', ''].map(h => (
|
||||
<th key={h} className="text-left px-4 py-3 text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide">
|
||||
{h}
|
||||
{COLUMNS.map(col => (
|
||||
<th
|
||||
key={col.label}
|
||||
className="text-left px-4 py-3 text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide"
|
||||
>
|
||||
{col.key ? (
|
||||
<button
|
||||
onClick={() => handleSort(col.key!)}
|
||||
className="flex items-center gap-1 hover:text-slate-700 dark:hover:text-slate-200 transition-colors"
|
||||
>
|
||||
{col.label}
|
||||
{sortKey === col.key
|
||||
? sortDir === 'asc'
|
||||
? <ArrowUp size={12} className="text-brand-600" />
|
||||
: <ArrowDown size={12} className="text-brand-600" />
|
||||
: <ArrowUpDown size={12} className="opacity-30" />
|
||||
}
|
||||
</button>
|
||||
) : col.label}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filtered.map(b => {
|
||||
{sorted.map(b => {
|
||||
const r = room(b.roomId)
|
||||
const nights = nightsCount(b.checkIn, b.checkOut)
|
||||
return (
|
||||
@@ -152,7 +203,7 @@ export function BookingsPage() {
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
{filtered.length === 0 && (
|
||||
{sorted.length === 0 && (
|
||||
<div className="text-center py-12 text-slate-500 dark:text-slate-400">
|
||||
Бронирования не найдены
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user