Wide BookingModal (2-col), RoomModal, RU channels, migration module, all modules active
- BookingModal: 2-column layout (size=2xl/max-w-3xl), no scroll needed; hourly mode toggle for rooms with allowHourly=true - RoomModal: full add/edit form with hourly rate toggle and amenities checkboxes; RoomsPage wired up - Channel manager: Яндекс Путешествия, Островок, Суточно.ру, OneTwoTrip added; split into RU/International sections - Migration module: 3-step wizard (source select → file upload → progress/results) - All modules set to active by default (version bump to reset localStorage) - BookingCalendar booking blocks: guest count badge (Xг), unpaid red dot indicator Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,17 +1,21 @@
|
||||
import { useState } from 'react'
|
||||
import { BedDouble, Users, Wifi, Plus, Search } from 'lucide-react'
|
||||
import { BedDouble, Users, Wifi, Plus, Search, Pencil } from 'lucide-react'
|
||||
import { MOCK_ROOMS } from '../data/mockData'
|
||||
import type { Room } from '../types'
|
||||
import { cn, ROOM_STATUS_COLORS, ROOM_STATUS_LABELS, HK_STATUS_COLORS, HK_STATUS_LABELS, formatCurrency } from '../lib/utils'
|
||||
import { Badge } from '../components/ui/Badge'
|
||||
import { RoomModal } from '../components/rooms/RoomModal'
|
||||
|
||||
export function RoomsPage() {
|
||||
const [rooms, setRooms] = useState<Room[]>(MOCK_ROOMS)
|
||||
const [search, setSearch] = useState('')
|
||||
const [floorFilter, setFloorFilter] = useState<number | 'all'>('all')
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const [editingRoom, setEditingRoom] = useState<Room | undefined>(undefined)
|
||||
|
||||
const floors = [...new Set(MOCK_ROOMS.map(r => r.floor))].sort()
|
||||
const floors = [...new Set(rooms.map(r => r.floor))].sort()
|
||||
|
||||
const filtered = MOCK_ROOMS.filter(r => {
|
||||
const filtered = rooms.filter(r => {
|
||||
const matchSearch = search === '' ||
|
||||
r.number.includes(search) ||
|
||||
r.type.toLowerCase().includes(search.toLowerCase()) ||
|
||||
@@ -21,10 +25,33 @@ export function RoomsPage() {
|
||||
})
|
||||
|
||||
const stats = {
|
||||
available: MOCK_ROOMS.filter(r => r.status === 'available').length,
|
||||
occupied: MOCK_ROOMS.filter(r => r.status === 'occupied').length,
|
||||
maintenance: MOCK_ROOMS.filter(r => r.status === 'maintenance').length,
|
||||
dirty: MOCK_ROOMS.filter(r => r.housekeepingStatus === 'dirty' || r.housekeepingStatus === 'cleaning').length,
|
||||
available: rooms.filter(r => r.status === 'available').length,
|
||||
occupied: rooms.filter(r => r.status === 'occupied').length,
|
||||
maintenance: rooms.filter(r => r.status === 'maintenance').length,
|
||||
dirty: rooms.filter(r => r.housekeepingStatus === 'dirty' || r.housekeepingStatus === 'cleaning').length,
|
||||
}
|
||||
|
||||
const openCreate = () => {
|
||||
setEditingRoom(undefined)
|
||||
setModalOpen(true)
|
||||
}
|
||||
|
||||
const openEdit = (room: Room) => {
|
||||
setEditingRoom(room)
|
||||
setModalOpen(true)
|
||||
}
|
||||
|
||||
const handleSave = (room: Room) => {
|
||||
setRooms(prev => {
|
||||
const idx = prev.findIndex(r => r.id === room.id)
|
||||
if (idx >= 0) {
|
||||
const updated = [...prev]
|
||||
updated[idx] = room
|
||||
return updated
|
||||
}
|
||||
return [...prev, room]
|
||||
})
|
||||
setModalOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -33,9 +60,9 @@ export function RoomsPage() {
|
||||
<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">{MOCK_ROOMS.length} номеров</p>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">{rooms.length} номеров</p>
|
||||
</div>
|
||||
<button className="btn-primary">
|
||||
<button className="btn-primary" onClick={openCreate}>
|
||||
<Plus size={15} />
|
||||
<span className="hidden sm:inline">Добавить номер</span>
|
||||
</button>
|
||||
@@ -98,16 +125,32 @@ export function RoomsPage() {
|
||||
{/* Room grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{filtered.map(room => (
|
||||
<RoomCard key={room.id} room={room} />
|
||||
<RoomCard key={room.id} room={room} onEdit={() => openEdit(room)} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Modal */}
|
||||
<RoomModal
|
||||
open={modalOpen}
|
||||
room={editingRoom}
|
||||
onClose={() => setModalOpen(false)}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RoomCard({ room }: { room: Room }) {
|
||||
function RoomCard({ room, onEdit }: { room: Room; onEdit: () => void }) {
|
||||
return (
|
||||
<div className="card p-4 hover:shadow-card-hover transition-shadow cursor-pointer group">
|
||||
<div className="card p-4 hover:shadow-card-hover transition-shadow cursor-pointer group relative">
|
||||
{/* Edit button on hover */}
|
||||
<button
|
||||
onClick={e => { e.stopPropagation(); onEdit() }}
|
||||
className="absolute top-3 right-3 p-1.5 rounded-lg bg-white dark:bg-slate-700 border border-slate-200 dark:border-slate-600 text-slate-500 opacity-0 group-hover:opacity-100 transition-opacity shadow-sm hover:text-brand-600"
|
||||
>
|
||||
<Pencil size={13} />
|
||||
</button>
|
||||
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -120,11 +163,11 @@ function RoomCard({ room }: { room: Room }) {
|
||||
</div>
|
||||
<span className="text-sm text-slate-500 dark:text-slate-400">{room.type} · Этаж {room.floor}</span>
|
||||
</div>
|
||||
<div className={cn('w-3 h-3 rounded-full mt-1', {
|
||||
<div className={cn('w-3 h-3 rounded-full mt-1 mr-6', {
|
||||
'bg-emerald-500': room.status === 'available',
|
||||
'bg-brand-500': room.status === 'occupied',
|
||||
'bg-orange-500': room.status === 'maintenance',
|
||||
'bg-slate-400': room.status === 'blocked',
|
||||
'bg-brand-500': room.status === 'occupied',
|
||||
'bg-orange-500': room.status === 'maintenance',
|
||||
'bg-slate-400': room.status === 'blocked',
|
||||
})} />
|
||||
</div>
|
||||
|
||||
@@ -158,7 +201,9 @@ function RoomCard({ room }: { room: Room }) {
|
||||
<span className="text-base font-bold text-slate-900 dark:text-slate-100">
|
||||
{formatCurrency(room.baseRate)}
|
||||
</span>
|
||||
<span className="text-xs text-slate-400 dark:text-slate-500">/ночь</span>
|
||||
<span className="text-xs text-slate-400 dark:text-slate-500">
|
||||
/ночь{room.allowHourly ? ` · ${formatCurrency(room.hourlyRate ?? 0)}/ч` : ''}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{room.amenities.length > 0 && (
|
||||
|
||||
Reference in New Issue
Block a user