Connect frontend to real API — rooms, bookings, housekeeping, calendar
- src/lib/api.ts: central API client with JWT auto-refresh, snake_case→camelCase transform - src/contexts/AuthContext.tsx: real login via POST /api/auth/login - src/pages/RoomsPage.tsx: load rooms from API, create/update via API - src/pages/BookingsPage.tsx: load bookings + rooms from API - src/pages/HousekeepingPage.tsx: load today's tasks from API, update status via API - src/pages/CalendarPage.tsx: load rooms + bookings from API - src/types/index.ts: fix HousekeepingTask.priority to match DB (medium/urgent) - backend/src/routes/rooms.ts: update to use new column names (max_guests, base_rate) + all new fields - backend/src/routes/bookings.ts: update price_per_night→base_rate, add paid_amount to PATCH - backend/migrations/003_fix_constraints.sql: fix rooms.status values, add paid_amount, inquiry status, other source - public/robots.txt + index.html: noindex for SPA inner pages Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { useState } from 'react'
|
||||
import { BedDouble, Users, Wifi, Plus, Search, Pencil, ChevronDown, ChevronUp, Check, Trash2, X as XIcon } from 'lucide-react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { BedDouble, Users, Wifi, Plus, Search, Pencil, ChevronDown, ChevronUp, Check, Trash2, X as XIcon, Loader2 } from 'lucide-react'
|
||||
import { useAmenities } from '../contexts/AmenitiesContext'
|
||||
import { MOCK_ROOMS } from '../data/mockData'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import { api } from '../lib/api'
|
||||
import { MOCK_CATEGORIES } from './RoomCategoriesPage'
|
||||
import type { Room } from '../types'
|
||||
import { cn, ROOM_STATUS_COLORS, ROOM_STATUS_LABELS, HK_STATUS_COLORS, HK_STATUS_LABELS, formatCurrency } from '../lib/utils'
|
||||
@@ -9,7 +10,11 @@ import { Badge } from '../components/ui/Badge'
|
||||
import { RoomModal } from '../components/rooms/RoomModal'
|
||||
|
||||
export function RoomsPage() {
|
||||
const [rooms, setRooms] = useState<Room[]>(MOCK_ROOMS)
|
||||
const { user } = useAuth()
|
||||
const slug = user?.hotelSlug ?? ''
|
||||
|
||||
const [rooms, setRooms] = useState<Room[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [search, setSearch] = useState('')
|
||||
const [floorFilter, setFloorFilter] = useState<number | 'all'>('all')
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
@@ -21,6 +26,14 @@ export function RoomsPage() {
|
||||
const [editingAmenity, setEditingAmenity] = useState<string | null>(null)
|
||||
const [editAmenityValue, setEditAmenityValue] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (!slug) return
|
||||
api.rooms.list(slug)
|
||||
.then(setRooms)
|
||||
.catch(console.error)
|
||||
.finally(() => setLoading(false))
|
||||
}, [slug])
|
||||
|
||||
const floors = [...new Set(rooms.map(r => r.floor))].sort()
|
||||
|
||||
const filtered = rooms.filter(r => {
|
||||
@@ -49,17 +62,44 @@ export function RoomsPage() {
|
||||
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
|
||||
const handleSave = async (room: Room) => {
|
||||
try {
|
||||
const isNew = !rooms.find(r => r.id === room.id)
|
||||
if (isNew) {
|
||||
const created = await api.rooms.create(slug, {
|
||||
number: room.number, type: room.type, floor: room.floor,
|
||||
maxGuests: room.maxGuests, baseRate: room.baseRate, status: room.status,
|
||||
amenities: room.amenities, name: room.name, categoryId: room.categoryId,
|
||||
bedType: room.bedType, beds: room.beds, housekeepingStatus: room.housekeepingStatus,
|
||||
sortOrder: room.sortOrder, allowHourly: room.allowHourly, hourlyRate: room.hourlyRate,
|
||||
extraPlace: room.extraPlace, childPolicy: room.childPolicy,
|
||||
description: room.description, photos: room.photos,
|
||||
})
|
||||
setRooms(prev => [...prev, created])
|
||||
} else {
|
||||
const updated = await api.rooms.update(slug, room.id, {
|
||||
number: room.number, type: room.type, floor: room.floor,
|
||||
maxGuests: room.maxGuests, baseRate: room.baseRate, status: room.status,
|
||||
amenities: room.amenities, name: room.name, categoryId: room.categoryId,
|
||||
bedType: room.bedType, beds: room.beds, housekeepingStatus: room.housekeepingStatus,
|
||||
sortOrder: room.sortOrder, allowHourly: room.allowHourly, hourlyRate: room.hourlyRate,
|
||||
extraPlace: room.extraPlace, childPolicy: room.childPolicy,
|
||||
description: room.description, photos: room.photos,
|
||||
})
|
||||
setRooms(prev => prev.map(r => r.id === updated.id ? updated : r))
|
||||
}
|
||||
return [...prev, room]
|
||||
})
|
||||
setModalOpen(false)
|
||||
setModalOpen(false)
|
||||
} catch (err) {
|
||||
console.error('Failed to save room:', err)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<Loader2 size={24} className="animate-spin text-brand-600" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -131,11 +171,17 @@ export function RoomsPage() {
|
||||
</div>
|
||||
|
||||
{/* 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} onEdit={() => openEdit(room)} />
|
||||
))}
|
||||
</div>
|
||||
{filtered.length === 0 && !loading ? (
|
||||
<div className="text-center py-16 text-slate-400 dark:text-slate-500">
|
||||
{rooms.length === 0 ? 'Нет номеров. Добавьте первый номер.' : 'Ничего не найдено.'}
|
||||
</div>
|
||||
) : (
|
||||
<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} onEdit={() => openEdit(room)} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Справочник удобств */}
|
||||
<div className="card overflow-hidden">
|
||||
@@ -156,7 +202,6 @@ export function RoomsPage() {
|
||||
|
||||
{showAmenities && (
|
||||
<div className="border-t border-slate-200 dark:border-slate-700 p-5 space-y-4">
|
||||
{/* Add new */}
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
@@ -177,7 +222,6 @@ export function RoomsPage() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* List */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2">
|
||||
{amenities.map(a => (
|
||||
<div
|
||||
@@ -242,7 +286,6 @@ export function RoomsPage() {
|
||||
function RoomCard({ room, onEdit }: { room: Room; onEdit: () => void }) {
|
||||
return (
|
||||
<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"
|
||||
|
||||
Reference in New Issue
Block a user