Connect ChannelsPage, FloorMapPage, SettingsPage, UsersPage to real API

- ChannelsPage: load from API, toggle/sync call real endpoints; normalize
  enabled→isEnabled, lastSyncedAt→lastSyncAt, add displayName/mappings defaults
- FloorMapPage: load rooms+bookings from API; create booking via API
- SettingsPage: load hotel via GET /api/hotels/:slug; save general section
  via PATCH (name, address, timezone, currency, check_in/out times)
- UsersPage: load users from API; create/update/delete via API;
  map backend User (name/role) → StaffUser (firstName/lastName/StaffRole)
- api.ts: add hotels.get/update, users.delete, channel normalization,
  HotelPayload/toHotelPayload
- types/index.ts: extend Hotel with phone/checkInTime/checkOutTime/optional fields;
  add User.createdAt/updatedAt
- backend/routes/hotels.ts: extend PATCH to allow address/phone/check_in_time/check_out_time
- backend/migrations/004_hotels_contacts.sql: add address/phone/check_in/out_time to hotels

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-17 14:21:45 +03:00
parent 57162c55c6
commit 55a34dc0dc
9 changed files with 277 additions and 109 deletions

View File

@@ -0,0 +1,6 @@
-- Migration 004 — Add contact/schedule fields to hotels
ALTER TABLE hotels
ADD COLUMN IF NOT EXISTS address TEXT,
ADD COLUMN IF NOT EXISTS phone VARCHAR(50),
ADD COLUMN IF NOT EXISTS check_in_time TIME NOT NULL DEFAULT '14:00',
ADD COLUMN IF NOT EXISTS check_out_time TIME NOT NULL DEFAULT '12:00';

View File

@@ -76,7 +76,7 @@ const hotels: FastifyPluginAsync = async (fastify) => {
return reply.code(403).send({ error: 'Forbidden' })
}
const allowed = ['name', 'plan', 'timezone', 'currency']
const allowed = ['name', 'plan', 'timezone', 'currency', 'address', 'phone', 'check_in_time', 'check_out_time']
const updates: string[] = []
const values: unknown[] = []
let idx = 1

View File

@@ -1,4 +1,4 @@
import type { Room, Booking, HousekeepingTask, Channel, User } from '../types'
import type { Room, Booking, HousekeepingTask, Channel, ChannelName, Hotel, User } from '../types'
// ── Base URL ────────────────────────────────────────────────────────────────
@@ -104,6 +104,34 @@ async function req<T>(
return transformKeys(data) as T
}
// ── Channel normalization ────────────────────────────────────────────────────
const CHANNEL_DISPLAY_NAMES: Record<string, string> = {
booking_com: 'Booking.com',
airbnb: 'Airbnb',
expedia: 'Expedia',
vrbo: 'VRBO',
yandex_travel: 'Яндекс Путешествия',
ostrovok: 'Островок',
sutochno: 'Суточно.ру',
onetwotrip: 'OneTwoTrip',
}
function normalizeChannel(raw: Record<string, unknown>): Channel {
const name = raw.name as ChannelName
return {
id: raw.id as string,
hotelId: (raw.hotelId as string) ?? '',
name,
displayName: CHANNEL_DISPLAY_NAMES[name as string] ?? String(name),
isEnabled: Boolean(raw.enabled),
lastSyncAt: (raw.lastSyncedAt as string | null) ?? null,
lastSyncStatus: raw.lastSyncedAt ? 'success' : 'idle',
bookingsImported: 0,
mappings: [],
}
}
// ── API methods ──────────────────────────────────────────────────────────────
export const api = {
@@ -177,14 +205,20 @@ export const api = {
// ── Channels ──────────────────────────────────────────────────────────────
channels: {
list: (slug: string) =>
req<Channel[]>('GET', `/api/hotels/${slug}/channels`),
list: async (slug: string): Promise<Channel[]> => {
const raw = await req<Record<string, unknown>[]>('GET', `/api/hotels/${slug}/channels`)
return raw.map(normalizeChannel)
},
update: (slug: string, id: string, data: { enabled?: boolean; api_key?: string }) =>
req<Channel>('PATCH', `/api/hotels/${slug}/channels/${id}`, data),
update: async (slug: string, id: string, data: { enabled?: boolean; api_key?: string }): Promise<Channel> => {
const raw = await req<Record<string, unknown>>('PATCH', `/api/hotels/${slug}/channels/${id}`, data)
return normalizeChannel(raw)
},
sync: (slug: string, id: string) =>
req<{ channel: Channel; synced_bookings: number }>('POST', `/api/hotels/${slug}/channels/${id}/sync`),
sync: async (slug: string, id: string): Promise<{ channel: Channel; syncedBookings: number }> => {
const raw = await req<Record<string, unknown>>('POST', `/api/hotels/${slug}/channels/${id}/sync`)
return { channel: normalizeChannel(raw), syncedBookings: (raw.syncedBookings as number) ?? 0 }
},
},
// ── Users ─────────────────────────────────────────────────────────────────
@@ -197,6 +231,18 @@ export const api = {
update: (slug: string, id: string, data: Partial<{ name: string; email: string; password: string }>) =>
req<User>('PATCH', `/api/hotels/${slug}/users/${id}`, data),
delete: (slug: string, id: string) =>
req<void>('DELETE', `/api/hotels/${slug}/users/${id}`),
},
// ── Hotels ────────────────────────────────────────────────────────────────
hotels: {
get: (slug: string) =>
req<Hotel>('GET', `/api/hotels/${slug}`),
update: (slug: string, data: HotelPayload) =>
req<Hotel>('PATCH', `/api/hotels/${slug}`, toHotelPayload(data)),
},
}
@@ -264,3 +310,21 @@ export interface HkPayload {
room_id?: string; type?: string; priority?: string
status?: string; assignee_id?: string; notes?: string; due_date?: string
}
export interface HotelPayload {
name?: string; address?: string; phone?: string
timezone?: string; currency?: string
checkInTime?: string; checkOutTime?: string
}
function toHotelPayload(h: HotelPayload): Record<string, unknown> {
const out: Record<string, unknown> = {}
if (h.name !== undefined) out.name = h.name
if (h.address !== undefined) out.address = h.address
if (h.phone !== undefined) out.phone = h.phone
if (h.timezone !== undefined) out.timezone = h.timezone
if (h.currency !== undefined) out.currency = h.currency
if (h.checkInTime !== undefined) out.check_in_time = h.checkInTime
if (h.checkOutTime !== undefined) out.check_out_time = h.checkOutTime
return out
}

View File

@@ -5,7 +5,7 @@ import { Badge } from '../components/ui/Badge'
export function AdminDashboard() {
const activeHotels = MOCK_HOTELS.filter(h => h.isActive).length
const totalRooms = MOCK_HOTELS.reduce((s, h) => s + h.roomCount, 0)
const totalRooms = MOCK_HOTELS.reduce((s, h) => s + (h.roomCount ?? 0), 0)
return (
<div className="p-4 md:p-6 space-y-6">

View File

@@ -1,6 +1,7 @@
import { useState } from 'react'
import { useState, useEffect } from 'react'
import { RefreshCw, CheckCircle2, XCircle, Clock, Globe, AlertTriangle } from 'lucide-react'
import { MOCK_CHANNELS } from '../data/mockData'
import { api } from '../lib/api'
import { useAuth } from '../contexts/AuthContext'
import type { Channel, SyncStatus } from '../types'
import { cn } from '../lib/utils'
import { Badge } from '../components/ui/Badge'
@@ -35,25 +36,40 @@ function SyncStatusBadge({ status }: { status: SyncStatus }) {
}
export function ChannelsPage() {
const [channels, setChannels] = useState<Channel[]>(MOCK_CHANNELS)
const { user } = useAuth()
const slug = user?.hotelSlug ?? ''
const [channels, setChannels] = useState<Channel[]>([])
const [loading, setLoading] = useState(true)
const triggerSync = (id: string) => {
setChannels(prev => prev.map(c =>
c.id === id ? { ...c, lastSyncStatus: 'syncing' } : c,
))
setTimeout(() => {
setChannels(prev => prev.map(c =>
c.id === id
? { ...c, lastSyncStatus: 'success', lastSyncAt: new Date().toISOString(), bookingsImported: c.bookingsImported + Math.floor(Math.random() * 3) }
: c,
))
}, 2000)
useEffect(() => {
if (!slug) return
api.channels.list(slug)
.then(setChannels)
.catch(console.error)
.finally(() => setLoading(false))
}, [slug])
const triggerSync = async (id: string) => {
setChannels(prev => prev.map(c => c.id === id ? { ...c, lastSyncStatus: 'syncing' } : c))
try {
const { channel } = await api.channels.sync(slug, id)
setChannels(prev => prev.map(c => c.id === id ? { ...c, ...channel, lastSyncStatus: 'success' } : c))
} catch {
setChannels(prev => prev.map(c => c.id === id ? { ...c, lastSyncStatus: 'error' } : c))
}
}
const toggleChannel = (id: string) => {
setChannels(prev => prev.map(c =>
c.id === id ? { ...c, isEnabled: !c.isEnabled } : c,
))
const toggleChannel = async (id: string) => {
const ch = channels.find(c => c.id === id)
if (!ch) return
const next = !ch.isEnabled
setChannels(prev => prev.map(c => c.id === id ? { ...c, isEnabled: next } : c))
try {
const updated = await api.channels.update(slug, id, { enabled: next })
setChannels(prev => prev.map(c => c.id === id ? updated : c))
} catch {
setChannels(prev => prev.map(c => c.id === id ? { ...c, isEnabled: !next } : c))
}
}
const totalImported = channels.reduce((s, c) => s + c.bookingsImported, 0)
@@ -62,6 +78,10 @@ export function ChannelsPage() {
const internationalChannels = channels.filter(c => !RUSSIAN_CHANNELS.has(c.name))
const russianChannels = channels.filter(c => RUSSIAN_CHANNELS.has(c.name))
if (loading) return (
<div className="p-6 flex items-center justify-center text-slate-400 text-sm">Загрузка...</div>
)
return (
<div className="p-4 md:p-6 space-y-5">
{/* Header */}

View File

@@ -1,24 +1,42 @@
import { useState } from 'react'
import { useState, useEffect } from 'react'
import { format, addDays } from 'date-fns'
import { Pencil, Check } from 'lucide-react'
import { MOCK_ROOMS, MOCK_BOOKINGS } from '../data/mockData'
import { api } from '../lib/api'
import { useAuth } from '../contexts/AuthContext'
import { FloorMap } from '../components/floormap/FloorMap'
import { BookingModal } from '../components/bookings/BookingModal'
import { cn } from '../lib/utils'
import type { Booking } from '../types'
import type { Room, Booking } from '../types'
import type { BookingPayload } from '../lib/api'
export function FloorMapPage() {
const [bookings, setBookings] = useState<Booking[]>(MOCK_BOOKINGS)
const { user } = useAuth()
const slug = user?.hotelSlug ?? ''
const [rooms, setRooms] = useState<Room[]>([])
const [bookings, setBookings] = useState<Booking[]>([])
const [loading, setLoading] = useState(true)
const [createForRoom, setCreateForRoom] = useState<string | null>(null)
const [editMode, setEditMode] = useState(false)
useEffect(() => {
if (!slug) return
Promise.all([api.rooms.list(slug), api.bookings.list(slug)])
.then(([r, b]) => { setRooms(r); setBookings(b) })
.catch(console.error)
.finally(() => setLoading(false))
}, [slug])
const stats = {
total: MOCK_ROOMS.length,
occupied: MOCK_ROOMS.filter(r => r.status === 'occupied').length,
available: MOCK_ROOMS.filter(r => r.status === 'available').length,
maintenance: MOCK_ROOMS.filter(r => r.status === 'maintenance' || r.status === 'blocked').length,
total: rooms.length,
occupied: rooms.filter(r => r.status === 'occupied').length,
available: rooms.filter(r => r.status === 'available').length,
maintenance: rooms.filter(r => r.status === 'maintenance' || r.status === 'blocked').length,
}
if (loading) return (
<div className="p-6 flex items-center justify-center text-slate-400 text-sm">Загрузка...</div>
)
return (
<div className="p-4 md:p-6 space-y-4">
{/* Header */}
@@ -63,7 +81,7 @@ export function FloorMapPage() {
{/* Floor map */}
<div className="card overflow-hidden p-0">
<FloorMap
rooms={MOCK_ROOMS}
rooms={rooms}
bookings={bookings}
editMode={editMode}
onSelectRoom={editMode ? undefined : (id) => setCreateForRoom(id)}
@@ -79,11 +97,15 @@ export function FloorMapPage() {
checkIn: format(new Date(), 'yyyy-MM-dd'),
checkOut: format(addDays(new Date(), 1), 'yyyy-MM-dd'),
}}
rooms={MOCK_ROOMS}
rooms={rooms}
onClose={() => setCreateForRoom(null)}
onSave={(data) => {
setBookings(prev => [...prev, data as Booking])
onSave={async (data) => {
try {
const created = await api.bookings.create(slug, data as BookingPayload)
setBookings(prev => [...prev, created])
} finally {
setCreateForRoom(null)
}
}}
/>
)}

View File

@@ -1,6 +1,8 @@
import { useState } from 'react'
import { Save, Building2, Bell, Shield, Globe, CreditCard, BedDouble, Mail, MessageSquare, Users, Plus, X as XIcon, Send, CheckCircle2, Copy } from 'lucide-react'
import { MOCK_HOTELS } from '../data/mockData'
import { useState, useEffect } from 'react'
import { Save, Building2, Bell, Shield, Globe, CreditCard, BedDouble, Mail, MessageSquare, Plus, X as XIcon, Send, CheckCircle2, Copy } from 'lucide-react'
import { api } from '../lib/api'
import { useAuth } from '../contexts/AuthContext'
import type { Hotel } from '../types'
import { cn, PLAN_COLORS, PLAN_LABELS } from '../lib/utils'
import { Badge } from '../components/ui/Badge'
import { useTheme } from '../contexts/ThemeContext'
@@ -26,11 +28,18 @@ function Toggle({ on, onChange }: { on: boolean; onChange: () => void }) {
}
export function SettingsPage() {
const { user } = useAuth()
const slug = user?.hotelSlug ?? ''
const [section, setSection] = useState('general')
const [saved, setSaved] = useState(false)
const hotel = MOCK_HOTELS[0]
const [hotel, setHotel] = useState<Hotel | null>(null)
const { theme, toggle } = useTheme()
useEffect(() => {
if (!slug) return
api.hotels.get(slug).then(setHotel).catch(console.error)
}, [slug])
const [calendarCompact, setCalendarCompact] = useState(
() => localStorage.getItem('calendarCompact') === 'true',
)
@@ -43,14 +52,26 @@ export function SettingsPage() {
}
const [form, setForm] = useState({
name: hotel.name,
address: hotel.address,
timezone: hotel.timezone,
currency: hotel.currency,
name: '',
address: '',
timezone: 'Europe/Moscow',
currency: 'RUB',
checkInTime: '14:00',
checkOutTime: '12:00',
})
useEffect(() => {
if (!hotel) return
setForm({
name: hotel.name ?? '',
address: hotel.address ?? '',
timezone: hotel.timezone ?? 'Europe/Moscow',
currency: hotel.currency ?? 'RUB',
checkInTime: hotel.checkInTime ?? '14:00',
checkOutTime: hotel.checkOutTime ?? '12:00',
})
}, [hotel])
// Booking / assignment settings
const [assignmentStrategy, setAssignmentStrategy] = useState<'spread' | 'together' | 'sequential' | 'manual'>('spread')
const [showBookingSource, setShowBookingSource] = useState(false)
@@ -126,7 +147,22 @@ export function SettingsPage() {
setTimeout(() => setCopiedToken(false), 2000)
}
const handleSave = () => {
const handleSave = async () => {
if (section === 'general') {
try {
const updated = await api.hotels.update(slug, {
name: form.name,
address: form.address,
timezone: form.timezone,
currency: form.currency,
checkInTime: form.checkInTime,
checkOutTime: form.checkOutTime,
})
setHotel(updated)
} catch (err) {
console.error('Failed to save hotel settings', err)
}
}
setSaved(true)
setTimeout(() => setSaved(false), 2000)
}
@@ -135,7 +171,7 @@ export function SettingsPage() {
<div className="p-4 md:p-6 max-w-4xl mx-auto">
<div className="mb-5">
<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">{hotel.name}</p>
<p className="text-sm text-slate-500 dark:text-slate-400">{hotel?.name ?? '...'}</p>
</div>
<div className="flex gap-5">
@@ -617,7 +653,7 @@ export function SettingsPage() {
<div>
<p className="text-sm text-slate-500 dark:text-slate-400">Текущий план</p>
<div className="flex items-center gap-2 mt-1">
<Badge className={PLAN_COLORS[hotel.plan]}>{PLAN_LABELS[hotel.plan]}</Badge>
<Badge className={PLAN_COLORS[hotel?.plan ?? 'starter']}>{PLAN_LABELS[hotel?.plan ?? 'starter']}</Badge>
</div>
</div>
<button className="btn-primary">Улучшить план</button>
@@ -630,11 +666,11 @@ export function SettingsPage() {
] as const).map(p => (
<div key={p.plan} className={cn(
'p-4 rounded-xl border-2 transition-all',
hotel.plan === p.plan ? 'border-brand-500 bg-brand-50/50 dark:bg-brand-900/10' : 'border-slate-200 dark:border-slate-600',
hotel?.plan === p.plan ? 'border-brand-500 bg-brand-50/50 dark:bg-brand-900/10' : 'border-slate-200 dark:border-slate-600',
)}>
<div className="flex items-center justify-between mb-2">
<Badge className={PLAN_COLORS[p.plan]}>{PLAN_LABELS[p.plan]}</Badge>
{hotel.plan === p.plan && <span className="text-xs text-brand-600 dark:text-brand-400 font-medium">Текущий</span>}
{hotel?.plan === p.plan && <span className="text-xs text-brand-600 dark:text-brand-400 font-medium">Текущий</span>}
</div>
<p className="text-lg font-bold text-slate-900 dark:text-slate-100 mb-3">{p.price}</p>
<ul className="space-y-1 text-xs text-slate-600 dark:text-slate-400">

View File

@@ -1,9 +1,12 @@
import { useState } from 'react'
import { useState, useEffect } from 'react'
import {
Plus, Pencil, Trash2, Search, Shield, User as UserIcon,
Sparkles, Mail, Phone, Eye, EyeOff, CheckCircle2, AlertCircle,
Lock, Check, X as XIcon,
} from 'lucide-react'
import { api } from '../lib/api'
import { useAuth } from '../contexts/AuthContext'
import type { User } from '../types'
import { cn } from '../lib/utils'
import { Modal } from '../components/ui/Modal'
@@ -168,45 +171,30 @@ const INITIAL_ROLE_PERMISSIONS: RolePermissions[] = [
},
]
// ── Mock data ──────────────────────────────────────────────────────────────────
// ── Helpers ────────────────────────────────────────────────────────────────────
const MOCK_USERS: StaffUser[] = [
{
id: 'u1', firstName: 'Александр', lastName: 'Петров',
email: 'manager@grand-palace.ru', phone: '+7 (999) 123-45-67',
role: 'hotel_manager', position: 'Управляющий',
isActive: true, createdAt: '2025-01-15', lastLogin: '2026-03-11',
avatarColor: '#4F46E5',
},
{
id: 'u2', firstName: 'Мария', lastName: 'Сидорова',
email: 'reception@grand-palace.ru', phone: '+7 (999) 234-56-78',
role: 'receptionist', position: 'Старший администратор',
isActive: true, createdAt: '2025-03-01', lastLogin: '2026-03-10',
avatarColor: '#059669',
},
{
id: 'u3', firstName: 'Наталья', lastName: 'Козлова',
email: 'cleaner@grand-palace.ru',
role: 'housekeeper', position: 'Старшая горничная',
isActive: true, createdAt: '2025-04-10', lastLogin: '2026-03-11',
avatarColor: '#2563EB',
},
{
id: 'u4', firstName: 'Дмитрий', lastName: 'Волков',
email: 'night@grand-palace.ru', phone: '+7 (999) 345-67-89',
role: 'receptionist', position: 'Ночной администратор',
isActive: false, createdAt: '2025-06-20',
avatarColor: '#7C3AED',
},
{
id: 'u5', firstName: 'Елена', lastName: 'Морозова',
email: 'accounting@grand-palace.ru',
role: 'accountant', position: 'Главный бухгалтер',
isActive: true, createdAt: '2025-02-01', lastLogin: '2026-03-09',
avatarColor: '#D97706',
},
]
function mapRole(r: string): StaffRole {
return r === 'housekeeper' ? 'housekeeper' : 'hotel_manager'
}
function toStaffUser(u: User): StaffUser {
const parts = u.name.trim().split(' ')
const firstName = parts[0] ?? ''
const lastName = parts.slice(1).join(' ')
const role = mapRole(u.role)
const colorIndex = Math.abs(u.id.charCodeAt(0) + u.id.charCodeAt(1)) % AVATAR_COLORS.length
return {
id: u.id,
firstName,
lastName,
email: u.email,
role,
position: DEFAULT_POSITIONS[role]?.[0] ?? '',
isActive: true,
createdAt: u.createdAt?.slice(0, 10) ?? '',
avatarColor: AVATAR_COLORS[colorIndex],
}
}
// ── User Modal ─────────────────────────────────────────────────────────────────
@@ -216,7 +204,7 @@ function UserModal({
open: boolean
user?: StaffUser
onClose: () => void
onSave: (u: StaffUser) => void
onSave: (u: StaffUser, password?: string) => void
}) {
const [form, setForm] = useState({
firstName: user?.firstName ?? '',
@@ -251,10 +239,10 @@ function UserModal({
if (!validate()) return
onSave({
...form,
id: user?.id ?? `u-${Date.now()}`,
id: user?.id ?? '',
createdAt: user?.createdAt ?? new Date().toISOString().slice(0, 10),
lastLogin: user?.lastLogin,
})
}, password || undefined)
}
const suggestions = DEFAULT_POSITIONS[form.role]
@@ -682,14 +670,23 @@ const PAGE_TABS = [
type PageTab = typeof PAGE_TABS[number]['id']
export function UsersPage() {
const { user: currentUser } = useAuth()
const slug = currentUser?.hotelSlug ?? ''
const [tab, setTab] = useState<PageTab>('staff')
const [users, setUsers] = useState<StaffUser[]>(MOCK_USERS)
const [users, setUsers] = useState<StaffUser[]>([])
const [search, setSearch] = useState('')
const [roleFilter, setRoleFilter] = useState<StaffRole | 'all'>('all')
const [modalOpen, setModalOpen] = useState(false)
const [editing, setEditing] = useState<StaffUser | undefined>()
const [deleteId, setDeleteId] = useState<string | null>(null)
useEffect(() => {
if (!slug) return
api.users.list(slug)
.then(list => setUsers(list.map(toStaffUser)))
.catch(console.error)
}, [slug])
const filtered = users.filter(u => {
const matchSearch = !search ||
`${u.firstName} ${u.lastName}`.toLowerCase().includes(search.toLowerCase()) ||
@@ -702,18 +699,35 @@ export function UsersPage() {
const openCreate = () => { setEditing(undefined); setModalOpen(true) }
const openEdit = (u: StaffUser) => { setEditing(u); setModalOpen(true) }
const handleSave = (u: StaffUser) => {
setUsers(prev => {
const idx = prev.findIndex(x => x.id === u.id)
if (idx >= 0) { const next = [...prev]; next[idx] = u; return next }
return [...prev, u]
const handleSave = async (u: StaffUser, password?: string) => {
const fullName = `${u.firstName} ${u.lastName}`.trim()
const backendRole = u.role === 'housekeeper' ? 'housekeeper' : 'manager'
try {
if (!u.id) {
const created = await api.users.create(slug, {
email: u.email, name: fullName, password: password ?? '', role: backendRole,
})
setUsers(prev => [...prev, toStaffUser(created)])
} else {
const upd: Partial<{ name: string; email: string; password: string }> = { name: fullName, email: u.email }
if (password) upd.password = password
const updated = await api.users.update(slug, u.id, upd)
setUsers(prev => prev.map(x => x.id === u.id ? { ...toStaffUser(updated), lastLogin: x.lastLogin } : x))
}
setModalOpen(false)
} catch (err) {
console.error('Failed to save user', err)
}
}
const handleDelete = (id: string) => {
const handleDelete = async (id: string) => {
try {
await api.users.delete(slug, id)
setUsers(prev => prev.filter(u => u.id !== id))
setDeleteId(null)
} catch (err) {
console.error('Failed to delete user', err)
}
}
const stats = {

View File

@@ -11,6 +11,8 @@ export interface User {
hotelName?: string
hotelSlug?: string
avatarUrl?: string
createdAt?: string
updatedAt?: string
}
export interface AuthSession {
@@ -26,13 +28,17 @@ export interface Hotel {
id: string
name: string
slug: string
address: string
address?: string
phone?: string
timezone: string
currency: string
checkInTime?: string
checkOutTime?: string
logoUrl?: string
plan: HotelPlan
isActive: boolean
roomCount: number
isActive?: boolean
roomCount?: number
activeBookings?: number
createdAt: string
}