Files
hotelsync/src/lib/api.ts
HotelSync e7ad9862ba feat: chat pagination — infinite scroll / load older messages
- Backend: cursor-based pagination via ?before=<ISO timestamp>
- Frontend: auto-load when scrolling to top (<60px), spinner + manual 'Загрузить ещё' button
- Poll merge: preserves older history when new messages arrive from polling
- hasMoreMsgs flag: shown only when exactly 50 msgs returned (more may exist)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 16:35:38 +03:00

1806 lines
67 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Room, Booking, HousekeepingTask, Channel, ChannelName, Hotel, User } from '../types'
// ── Base URL ────────────────────────────────────────────────────────────────
const BASE = (import.meta.env.VITE_API_URL as string | undefined) ?? 'https://api.hotelsync.ru'
// ── Errors ──────────────────────────────────────────────────────────────────
export class ApiError extends Error {
errors?: string[]
constructor(public status: number, message: string, errors?: string[]) {
super(message)
this.errors = errors
}
}
// ── Token helpers ────────────────────────────────────────────────────────────
function getToken(): string | null {
try {
const s = localStorage.getItem('hotelsync-session')
return s ? (JSON.parse(s) as { token: string }).token : null
} catch {
return null
}
}
function saveToken(token: string) {
try {
const s = localStorage.getItem('hotelsync-session')
if (!s) return
const parsed = JSON.parse(s) as Record<string, unknown>
parsed.token = token
localStorage.setItem('hotelsync-session', JSON.stringify(parsed))
// Notify AuthContext so React state stays in sync (WS reconnects with fresh token)
window.dispatchEvent(new CustomEvent('hotelsync:token-updated', { detail: { token } }))
} catch {
// ignore
}
}
// ── Snake ↔ camelCase transform ──────────────────────────────────────────────
function toCamel(s: string): string {
return s.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase())
}
function toSnake(s: string): string {
return s.replace(/[A-Z]/g, c => '_' + c.toLowerCase())
}
function transformKeys(val: unknown): unknown {
if (Array.isArray(val)) return val.map(transformKeys)
if (val && typeof val === 'object' && !(val instanceof Date)) {
const result: Record<string, unknown> = {}
for (const [k, v] of Object.entries(val as Record<string, unknown>)) {
result[toCamel(k)] = transformKeys(v)
}
return result
}
return val
}
function transformKeysToSnake(val: unknown): unknown {
if (Array.isArray(val)) return val.map(transformKeysToSnake)
if (val && typeof val === 'object' && !(val instanceof Date)) {
const result: Record<string, unknown> = {}
for (const [k, v] of Object.entries(val as Record<string, unknown>)) {
result[toSnake(k)] = transformKeysToSnake(v)
}
return result
}
return val
}
// ── Core request ─────────────────────────────────────────────────────────────
async function req<T>(
method: string,
path: string,
body?: unknown,
): Promise<T> {
const headers: Record<string, string> = {}
if (body !== undefined) headers['Content-Type'] = 'application/json'
const token = getToken()
if (token) headers['Authorization'] = `Bearer ${token}`
const doFetch = (t: string | null) =>
fetch(`${BASE}${path}`, {
method,
headers: t ? { ...headers, Authorization: `Bearer ${t}` } : headers,
credentials: 'include',
body: body !== undefined ? JSON.stringify(transformKeysToSnake(body)) : undefined,
})
let res = await doFetch(token)
// Auto-refresh on 401
if (res.status === 401) {
const refreshRes = await fetch(`${BASE}/api/auth/refresh`, {
method: 'POST',
credentials: 'include',
})
if (refreshRes.ok) {
const { access_token } = (await refreshRes.json()) as { access_token: string }
saveToken(access_token)
res = await doFetch(access_token)
} else {
localStorage.removeItem('hotelsync-session')
window.location.href = '/login'
throw new ApiError(401, 'Session expired')
}
}
if (res.status === 204) return undefined as T
const data: unknown = await res.json()
if (!res.ok) {
const d = data as Record<string, unknown>
const msg = (d?.error as string) ?? 'Request failed'
const errors = Array.isArray(d?.errors) ? (d.errors as string[]) : undefined
throw new ApiError(res.status, msg, errors)
}
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 normalizeRoom(r: Room): Room {
return {
...r,
baseRate: Number(r.baseRate) || 0,
hourlyRate: r.hourlyRate != null ? Number(r.hourlyRate) : undefined,
floor: Number(r.floor) || 0,
}
}
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 = {
// ── Auth ─────────────────────────────────────────────────────────────────
auth: {
login: (email: string, password: string) =>
req<{ access_token: string; user: User }>('POST', '/api/auth/login', { email, password }),
logout: () =>
req<void>('POST', '/api/auth/logout'),
me: () =>
req<User>('GET', '/api/auth/me'),
register: (data: {
hotelName: string
address: string
contact: string
email: string
phone: string
password: string
}) =>
// Snake_case вручную — req() конвертирует camelCase→snake_case автоматически
req<{ ok: boolean; message: string }>('POST', '/api/auth/register', {
hotel_name: data.hotelName,
address: data.address,
contact: data.contact,
email: data.email,
phone: data.phone,
password: data.password,
}),
forgotPassword: (email: string) =>
req<{ ok: boolean }>('POST', '/api/auth/forgot-password', { email }),
resetPassword: (token: string, password: string) =>
req<{ ok: boolean }>('POST', '/api/auth/reset-password', { token, password }),
resendConfirmation: (email: string) =>
req<{ ok: boolean }>('POST', '/api/auth/resend-confirmation', { email }),
},
// ── Rooms ─────────────────────────────────────────────────────────────────
rooms: {
list: (slug: string) =>
req<Room[]>('GET', `/api/hotels/${slug}/rooms`).then(rooms => rooms.map(normalizeRoom)),
create: (slug: string, data: RoomPayload) =>
req<Room>('POST', `/api/hotels/${slug}/rooms`, toRoomPayload(data)).then(normalizeRoom),
update: (slug: string, id: string, data: Partial<RoomPayload>) =>
req<Room>('PATCH', `/api/hotels/${slug}/rooms/${id}`, toRoomPayload(data)).then(normalizeRoom),
delete: (slug: string, id: string) =>
req<void>('DELETE', `/api/hotels/${slug}/rooms/${id}`),
},
// ── Bookings ──────────────────────────────────────────────────────────────
bookings: {
list: (slug: string, params?: { start?: string; end?: string; status?: string }) => {
const qs = new URLSearchParams()
if (params?.start) qs.set('start', params.start)
if (params?.end) qs.set('end', params.end)
if (params?.status) qs.set('status', params.status)
const q = qs.toString()
return req<Booking[]>('GET', `/api/hotels/${slug}/bookings${q ? `?${q}` : ''}`)
},
create: (slug: string, data: BookingPayload) =>
req<Booking>('POST', `/api/hotels/${slug}/bookings`, toBookingPayload(data)),
update: (slug: string, id: string, data: Partial<BookingPayload>) =>
req<Booking>('PATCH', `/api/hotels/${slug}/bookings/${id}`, toBookingPayload(data)),
delete: (slug: string, id: string) =>
req<void>('DELETE', `/api/hotels/${slug}/bookings/${id}`),
},
// ── Housekeeping ──────────────────────────────────────────────────────────
housekeeping: {
list: (slug: string, params?: { date?: string; status?: string; category?: string; roomId?: string }) => {
const qs = new URLSearchParams()
if (params?.date) qs.set('date', params.date)
if (params?.status) qs.set('status', params.status)
if (params?.category) qs.set('category', params.category)
if (params?.roomId) qs.set('room_id', params.roomId)
const q = qs.toString()
return req<HousekeepingTask[]>('GET', `/api/hotels/${slug}/housekeeping${q ? `?${q}` : ''}`)
},
create: (slug: string, data: HkPayload) =>
req<HousekeepingTask>('POST', `/api/hotels/${slug}/housekeeping`, data),
update: (slug: string, id: string, data: Partial<HkPayload>) =>
req<HousekeepingTask>('PATCH', `/api/hotels/${slug}/housekeeping/${id}`, data),
delete: (slug: string, id: string) =>
req<void>('DELETE', `/api/hotels/${slug}/housekeeping/${id}`),
getSettings: (slug: string) =>
req<HkSettings>('GET', `/api/hotels/${slug}/housekeeping-settings`),
saveSettings: (slug: string, data: Partial<HkSettings>) =>
req<HkSettings>('PATCH', `/api/hotels/${slug}/housekeeping-settings`, data),
},
// ── Channels ──────────────────────────────────────────────────────────────
channels: {
list: async (slug: string): Promise<Channel[]> => {
const raw = await req<Record<string, unknown>[]>('GET', `/api/hotels/${slug}/channels`)
return raw.map(normalizeChannel)
},
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: 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 ─────────────────────────────────────────────────────────────────
users: {
list: (slug: string) =>
req<User[]>('GET', `/api/hotels/${slug}/users`),
create: (slug: string, data: { email: string; password: string; name: string; role: string; phone?: string; position?: string }) =>
req<User>('POST', `/api/hotels/${slug}/users`, data),
update: (slug: string, id: string, data: Partial<{ name: string; email: string; password: string; role: string; phone: string; position: string; active: boolean }>) =>
req<User>('PATCH', `/api/hotels/${slug}/users/${id}`, data),
delete: (slug: string, id: string) =>
req<void>('DELETE', `/api/hotels/${slug}/users/${id}`),
},
// ── Schedule ──────────────────────────────────────────────────────────────
schedule: {
list: (slug: string, from: string, to: string) =>
req<ScheduleEntry[]>('GET', `/api/hotels/${slug}/schedule?from=${from}&to=${to}`),
upsert: (slug: string, data: {
user_id: string; date: string; shift_start?: string; shift_end?: string;
is_day_off?: boolean; notes?: string
}) =>
req<ScheduleEntry>('PUT', `/api/hotels/${slug}/schedule`, data),
remove: (slug: string, userId: string, date: string) =>
req<void>('DELETE', `/api/hotels/${slug}/schedule/${userId}/${date}`),
},
// ── Loyalty ───────────────────────────────────────────────────────────────
loyalty: {
getSettings: (slug: string) =>
req<LoyaltySettings>('GET', `/api/hotels/${slug}/loyalty/settings`),
saveSettings: (slug: string, data: Partial<LoyaltySettings>) =>
req<LoyaltySettings>('PATCH', `/api/hotels/${slug}/loyalty/settings`, data),
listGuests: (slug: string, q?: string) =>
req<LoyaltyGuestApi[]>('GET', `/api/hotels/${slug}/loyalty/guests${q ? `?q=${encodeURIComponent(q)}` : ''}`),
addPoints: (slug: string, guestId: string, data: { amount: number; reason: string; notes?: string }) =>
req<LoyaltyGuestApi>('POST', `/api/hotels/${slug}/loyalty/guests/${guestId}/points`, data),
listTransactions: (slug: string, limit = 50) =>
req<LoyaltyTransaction[]>('GET', `/api/hotels/${slug}/loyalty/transactions?limit=${limit}`),
},
// ── Chat ──────────────────────────────────────────────────────────────────
chat: {
listRooms: (slug: string) =>
req<ChatRoom[]>('GET', `/api/hotels/${slug}/chat/rooms`),
getMessages: (slug: string, roomId: string, limit = 50, before?: string) => {
const qs = before ? `?limit=${limit}&before=${encodeURIComponent(before)}` : `?limit=${limit}`
return req<ChatMessage[]>('GET', `/api/hotels/${slug}/chat/rooms/${roomId}/messages${qs}`)
},
sendMessage: (slug: string, roomId: string, text: string, attachmentUrl?: string) =>
req<ChatMessage>('POST', `/api/hotels/${slug}/chat/rooms/${roomId}/messages`, { text, ...(attachmentUrl ? { attachment_url: attachmentUrl } : {}) }),
uploadImage: async (file: File): Promise<{ url: string }> => {
const form = new FormData()
form.append('file', file)
const token = getToken()
const res = await fetch(`${BASE}/api/upload?folder=chat`, {
method: 'POST',
headers: token ? { Authorization: `Bearer ${token}` } : {},
credentials: 'include',
body: form,
})
if (!res.ok) throw new ApiError(res.status, 'Upload failed')
return res.json() as Promise<{ url: string }>
},
markRead: (slug: string, roomId: string) =>
req<{ ok: boolean }>('PATCH', `/api/hotels/${slug}/chat/rooms/${roomId}/read`),
openDirect: (slug: string, otherUserId: string) =>
req<{ roomId: string }>('POST', `/api/hotels/${slug}/chat/direct/${otherUserId}`),
editMessage: (slug: string, roomId: string, msgId: string, text: string) =>
req<ChatMessage>('PATCH', `/api/hotels/${slug}/chat/rooms/${roomId}/messages/${msgId}`, { text }),
deleteMessage: (slug: string, roomId: string, msgId: string) =>
req<{ ok: boolean; id: string }>('DELETE', `/api/hotels/${slug}/chat/rooms/${roomId}/messages/${msgId}`),
toggleReaction: (slug: string, roomId: string, msgId: string, emoji: string) =>
req<ChatMessage>('POST', `/api/hotels/${slug}/chat/rooms/${roomId}/messages/${msgId}/reactions`, { emoji }),
search: (slug: string, q: string) =>
req<ChatSearchResult[]>('GET', `/api/hotels/${slug}/chat/search?q=${encodeURIComponent(q)}`),
notify: (slug: string, text: string, systemName?: string) =>
req<ChatMessage>('POST', `/api/hotels/${slug}/chat/notify`, { text, system_name: systemName }),
setPresence: (slug: string) =>
req<{ ok: boolean }>('POST', `/api/hotels/${slug}/chat/presence`, {}),
getPresence: (slug: string) =>
req<string[]>('GET', `/api/hotels/${slug}/chat/presence`),
setTyping: (slug: string, roomId: string, typing: boolean) =>
req<{ ok: boolean }>('POST', `/api/hotels/${slug}/chat/rooms/${roomId}/typing`, { typing }),
getTyping: (slug: string, roomId: string) =>
req<string[]>('GET', `/api/hotels/${slug}/chat/rooms/${roomId}/typing`),
},
// ── Workstations ──────────────────────────────────────────────────────────
workstations: {
list: (slug: string) =>
req<Workstation[]>('GET', `/api/hotels/${slug}/workstations`),
create: (slug: string, name: string) =>
req<Workstation>('POST', `/api/hotels/${slug}/workstations`, { name }),
update: (slug: string, id: string, data: { name?: string; ttlockComPort?: string | null }) =>
req<Workstation>('PATCH', `/api/hotels/${slug}/workstations/${id}`, {
name: data.name,
ttlock_com_port: data.ttlockComPort,
}),
remove: (slug: string, id: string) =>
req<void>('DELETE', `/api/hotels/${slug}/workstations/${id}`),
pairCode: (slug: string, id: string) =>
req<{ code: string; expiresAt: string }>('POST', `/api/hotels/${slug}/workstations/${id}/pair-code`),
listDevices: (slug: string, id: string) =>
req<WorkstationDevice[]>('GET', `/api/hotels/${slug}/workstations/${id}/devices`),
addDevice: (slug: string, id: string, data: Partial<WorkstationDevice>) =>
req<WorkstationDevice>('POST', `/api/hotels/${slug}/workstations/${id}/devices`, data),
updateDevice: (slug: string, id: string, deviceId: string, data: Partial<WorkstationDevice>) =>
req<WorkstationDevice>('PATCH', `/api/hotels/${slug}/workstations/${id}/devices/${deviceId}`, data),
removeDevice: (slug: string, id: string, deviceId: string) =>
req<void>('DELETE', `/api/hotels/${slug}/workstations/${id}/devices/${deviceId}`),
testDevice: (slug: string, wsId: string, deviceId: string) =>
req<{ ok: boolean; error?: string; message?: string; kkt?: Record<string, unknown>; shift?: Record<string, unknown> }>('POST', `/api/hotels/${slug}/workstations/${wsId}/test-device/${deviceId}`),
sendUpdate: (slug: string, wsId: string) =>
req<{ ok: true }>('POST', `/api/hotels/${slug}/workstations/${wsId}/update`),
updateAll: (slug: string) =>
req<{ ok: true; sent: number }>('POST', `/api/hotels/${slug}/workstations/update-all`),
listPorts: (slug: string, wsId: string) =>
req<{ ports: { port: string; description?: string }[] }>('GET', `/api/hotels/${slug}/workstations/${wsId}/ports`),
listPrinters: (slug: string, wsId: string) =>
req<{ printers: { name: string; isDefault: boolean }[] }>('GET', `/api/hotels/${slug}/workstations/${wsId}/printers`),
ping: (slug: string, wsId: string, host: string) =>
req<{ ok: boolean; output?: string; error?: string }>('POST', `/api/hotels/${slug}/workstations/${wsId}/ping`, { host }),
},
// ── Agents ────────────────────────────────────────────────────────────────
agents: {
getLatestRelease: () => req<{ version: string; fileName: string; downloadUrl: string }>('GET', '/api/agents/latest-release'),
},
// ── Hotels ────────────────────────────────────────────────────────────────
hotels: {
get: (slug: string) =>
req<Hotel>('GET', `/api/hotels/${slug}`),
update: (slug: string, data: HotelPayload) =>
req<Hotel>('PATCH', `/api/hotels/${slug}`, toHotelPayload(data)),
},
// ── Booking Guests ────────────────────────────────────────────────────────
bookingGuests: {
list: (slug: string, bookingId: string) =>
req<BookingGuest[]>('GET', `/api/hotels/${slug}/bookings/${bookingId}/guests`),
add: (slug: string, bookingId: string, data: BookingGuestPayload) =>
req<BookingGuest>('POST', `/api/hotels/${slug}/bookings/${bookingId}/guests`, data),
update: (slug: string, bookingId: string, id: string, data: BookingGuestPayload) =>
req<BookingGuest>('PATCH', `/api/hotels/${slug}/bookings/${bookingId}/guests/${id}`, data),
remove: (slug: string, bookingId: string, id: string) =>
req<void>('DELETE', `/api/hotels/${slug}/bookings/${bookingId}/guests/${id}`),
},
// ── Hotel Settings ────────────────────────────────────────────────────────
hotelSettings: {
get: (slug: string) =>
req<HotelSettings>('GET', `/api/hotels/${slug}/hotel-settings`),
update: (slug: string, data: Partial<HotelSettings>) =>
req<{ ok: boolean }>('PATCH', `/api/hotels/${slug}/hotel-settings`, data),
},
// ── Guests ────────────────────────────────────────────────────────────────
guests: {
list: (slug: string, q?: string, passport?: string) => {
const params = passport
? `?passport=${encodeURIComponent(passport)}`
: q ? `?q=${encodeURIComponent(q)}` : ''
return req<GuestApiType[]>('GET', `/api/hotels/${slug}/guests${params}`)
},
get: (slug: string, id: string) =>
req<GuestApiType>('GET', `/api/hotels/${slug}/guests/${id}`),
create: (slug: string, data: GuestPayload) =>
req<GuestApiType>('POST', `/api/hotels/${slug}/guests`, data),
update: (slug: string, id: string, data: Partial<GuestPayload>) =>
req<GuestApiType>('PATCH', `/api/hotels/${slug}/guests/${id}`, data),
delete: (slug: string, id: string) =>
req<void>('DELETE', `/api/hotels/${slug}/guests/${id}`),
},
// ── Rental ────────────────────────────────────────────────────────────────
rental: {
listObjects: (slug: string) =>
req<RentalObjectApi[]>('GET', `/api/hotels/${slug}/rental-objects`),
createObject: (slug: string, data: RentalObjectPayload) =>
req<RentalObjectApi>('POST', `/api/hotels/${slug}/rental-objects`, data),
updateObject: (slug: string, id: string, data: Partial<RentalObjectPayload>) =>
req<RentalObjectApi>('PATCH', `/api/hotels/${slug}/rental-objects/${id}`, data),
deleteObject: (slug: string, id: string) =>
req<void>('DELETE', `/api/hotels/${slug}/rental-objects/${id}`),
listBookings: (slug: string, from?: string, to?: string) => {
const qs = new URLSearchParams()
if (from) qs.set('from', from)
if (to) qs.set('to', to)
const q = qs.toString()
return req<RentalBookingApi[]>('GET', `/api/hotels/${slug}/rental-bookings${q ? `?${q}` : ''}`)
},
createBooking: (slug: string, data: RentalBookingPayload) =>
req<RentalBookingApi>('POST', `/api/hotels/${slug}/rental-bookings`, data),
updateBooking: (slug: string, id: string, data: Partial<RentalBookingPayload>) =>
req<RentalBookingApi>('PATCH', `/api/hotels/${slug}/rental-bookings/${id}`, data),
deleteBooking: (slug: string, id: string) =>
req<void>('DELETE', `/api/hotels/${slug}/rental-bookings/${id}`),
},
// ── File Upload ───────────────────────────────────────────────────────────
upload: {
photo: async (file: File, folder: 'categories' | 'rooms' | 'hotels' | 'guests' | 'tasks' = 'rooms'): Promise<string> => {
const token = getToken()
const fd = new FormData()
fd.append('file', file)
const res = await fetch(`${BASE}/api/upload?folder=${folder}`, {
method: 'POST',
headers: token ? { Authorization: `Bearer ${token}` } : {},
body: fd,
})
if (!res.ok) throw new Error(`Upload failed: ${res.statusText}`)
const data = await res.json() as { url: string }
return data.url
},
deletePhoto: async (url: string): Promise<void> => {
await req('DELETE', `/api/upload?url=${encodeURIComponent(url)}`)
},
},
// ── Categories ────────────────────────────────────────────────────────────
categories: {
list: (slug: string) =>
req<CategoryApi[]>('GET', `/api/hotels/${slug}/categories`),
create: (slug: string, data: CategoryPayload) =>
req<CategoryApi>('POST', `/api/hotels/${slug}/categories`, data),
update: (slug: string, id: string, data: Partial<CategoryPayload>) =>
req<CategoryApi>('PATCH', `/api/hotels/${slug}/categories/${id}`, data),
delete: (slug: string, id: string) =>
req<void>('DELETE', `/api/hotels/${slug}/categories/${id}`),
},
// ── Tariffs ───────────────────────────────────────────────────────────────
tariffs: {
list: (slug: string) =>
req<TariffApi[]>('GET', `/api/hotels/${slug}/tariffs`),
create: (slug: string, data: TariffPayload) =>
req<TariffApi>('POST', `/api/hotels/${slug}/tariffs`, data),
update: (slug: string, id: string, data: Partial<TariffPayload>) =>
req<TariffApi>('PATCH', `/api/hotels/${slug}/tariffs/${id}`, data),
delete: (slug: string, id: string) =>
req<void>('DELETE', `/api/hotels/${slug}/tariffs/${id}`),
},
// ── Rate Overrides (per-cell prices) ─────────────────────────────────────
rateOverrides: {
list: (slug: string) =>
req<RateOverrideApi[]>('GET', `/api/hotels/${slug}/rate-overrides`),
bulkUpsert: (slug: string, overrides: RateOverridePayload[]) =>
req<{ count: number }>('POST', `/api/hotels/${slug}/rate-overrides/bulk`, { overrides }),
},
// ── Notifications ─────────────────────────────────────────────────────────
notifications: {
list: (slug: string) =>
req<Record<string, unknown>[]>('GET', `/api/hotels/${slug}/notifications`),
markRead: (slug: string, id: string) =>
req<Record<string, unknown>>('PATCH', `/api/hotels/${slug}/notifications/${id}`),
markAllRead: (slug: string) =>
req<{ ok: boolean }>('POST', `/api/hotels/${slug}/notifications/read-all`),
delete: (slug: string, id: string) =>
req<void>('DELETE', `/api/hotels/${slug}/notifications/${id}`),
},
// ── Rate Periods ─────────────────────────────────────────────────────────
ratePeriods: {
list: (slug: string) =>
req<RatePeriodApi[]>('GET', `/api/hotels/${slug}/rate-periods`),
create: (slug: string, data: RatePeriodPayload) =>
req<RatePeriodApi>('POST', `/api/hotels/${slug}/rate-periods`, data),
update: (slug: string, id: string, data: Partial<RatePeriodPayload>) =>
req<RatePeriodApi>('PATCH', `/api/hotels/${slug}/rate-periods/${id}`, data),
delete: (slug: string, id: string) =>
req<void>('DELETE', `/api/hotels/${slug}/rate-periods/${id}`),
},
// ── NetUP IPTV ────────────────────────────────────────────────────────────
netup: {
getSettings: (slug: string) =>
req<{ serverUrl: string; username: string; password: string; defaultLanguage: string; enabled: boolean; tlToken: string; connectionType: string; agentWorkstationId: string | null }>(
'GET', `/api/hotels/${slug}/netup/settings`),
saveSettings: (slug: string, data: { server_url: string; username: string; password?: string; default_language: string; enabled: boolean; tl_token: string; connection_type: string; agent_workstation_id: string | null }) =>
req<{ ok: boolean }>('PATCH', `/api/hotels/${slug}/netup/settings`, data),
testConnection: (slug: string) =>
req<{ ok: boolean; message: string }>('POST', `/api/hotels/${slug}/netup/test`),
getRoomMappings: (slug: string) =>
req<{ id: string; number: string; type: string; netupRoomNumber: string }[]>(
'GET', `/api/hotels/${slug}/netup/rooms`),
saveRoomMappings: (slug: string, mappings: { pms_room_id: string; netup_room_number: string }[]) =>
req<{ ok: boolean }>('POST', `/api/hotels/${slug}/netup/rooms`, { mappings }),
sendMessage: (slug: string, roomId: string, message: string, guestName?: string) =>
req<{ ok: boolean }>('POST', `/api/hotels/${slug}/netup/message`, { room_id: roomId, message, guest_name: guestName }),
getLog: (slug: string) =>
req<{
pushEvents: { ts: string; action: string; roomNumber: string; netupRoom: string; url: string; requestBody?: unknown; status: 'ok' | 'error' | 'skipped'; httpStatus?: number; responseBody?: string; error?: string }[]
pullRequests: { ts: string; method: string; url: string; headers: Record<string, unknown>; query: Record<string, unknown>; body: unknown }[]
}>('GET', `/api/hotels/${slug}/netup/log`),
clearLog: (slug: string) =>
req<{ ok: boolean }>('DELETE', `/api/hotels/${slug}/netup/log`),
testCheckin: (slug: string, roomId: string) =>
req<{ ok: boolean; message: string }>('POST', `/api/hotels/${slug}/netup/test-checkin`, { room_id: roomId }),
},
// ── WiFi Settings ─────────────────────────────────────────────────────────
wifi: {
get: (slug: string) =>
req<WifiSettings>('GET', `/api/hotels/${slug}/wifi-settings`),
update: (slug: string, data: Partial<Omit<WifiSettings, 'id' | 'hotelId' | 'apiToken'>>) =>
req<WifiSettings>('PATCH', `/api/hotels/${slug}/wifi-settings`, data),
regenerateToken: (slug: string) =>
req<{ apiToken: string }>('POST', `/api/hotels/${slug}/wifi-settings/regenerate-token`),
},
// ── TTLock ─────────────────────────────────────────────────────────────────
ttlock: {
getConfig: (slug: string) =>
req<TTLockConfig>('GET', `/api/hotels/${slug}/ttlock/config`),
updateConfig: (slug: string, data: TTLockConfigUpdate) =>
req<{ ok: boolean }>('PATCH', `/api/hotels/${slug}/ttlock/config`, data),
testEncoder: (slug: string, workstationId: string) =>
req<{ ok: boolean }>('POST', `/api/hotels/${slug}/ttlock/test`, { workstationId }),
testApi: (slug: string, workstationId: string) =>
req<{ ok: boolean }>('POST', `/api/hotels/${slug}/ttlock/test-api`, { workstation_id: workstationId }),
findApiConfig: (slug: string, workstationId: string) =>
req<{ ok: boolean; appDir?: string; results?: { path: string; matches: string[] }[] }>('POST', `/api/hotels/${slug}/ttlock/find-api-config`, { workstation_id: workstationId }),
dllInfo: (slug: string, workstationId: string) =>
req<{ ok: boolean; content: string; path: string }>('GET', `/api/hotels/${slug}/ttlock/dll-info?workstation_id=${workstationId}`),
getRoomLocks: (slug: string) =>
req<RoomLockMapping[]>('GET', `/api/hotels/${slug}/ttlock/room-locks`),
mapRoom: (slug: string, data: { roomId?: string; lockMac: string; lockName?: string; buildNo?: number; floorNo?: number }) =>
req<{ ok: boolean }>('POST', `/api/hotels/${slug}/ttlock/room-locks`, data),
unmapRoom: (slug: string, roomId: string) =>
req<{ ok: boolean }>('DELETE', `/api/hotels/${slug}/ttlock/room-locks/${roomId}`),
issueCard: (slug: string, bookingId: string, workstationId?: string) =>
req<CardIssuanceResult>('POST', `/api/hotels/${slug}/bookings/${bookingId}/issue-card`, workstationId ? { workstationId } : undefined),
getCards: (slug: string, bookingId: string) =>
req<CardIssuance[]>('GET', `/api/hotels/${slug}/bookings/${bookingId}/cards`),
},
// ── Checklists ────────────────────────────────────────────────────────────
checklists: {
listTemplates: (slug: string) =>
req<ChecklistTemplate[]>('GET', `/api/hotels/${slug}/checklist-templates`),
createTemplate: (slug: string, data: { name: string; taskType?: string }) =>
req<ChecklistTemplate>('POST', `/api/hotels/${slug}/checklist-templates`, {
name: data.name,
task_type: data.taskType ?? null,
}),
updateTemplate: (slug: string, templateId: string, data: { name?: string; taskType?: string | null; isActive?: boolean; sortOrder?: number; requireBeforeComplete?: boolean }) =>
req<ChecklistTemplate>('PATCH', `/api/hotels/${slug}/checklist-templates/${templateId}`, {
name: data.name,
task_type: data.taskType,
is_active: data.isActive,
sort_order: data.sortOrder,
require_before_complete: data.requireBeforeComplete,
}),
deleteTemplate: (slug: string, templateId: string) =>
req<void>('DELETE', `/api/hotels/${slug}/checklist-templates/${templateId}`),
addItem: (slug: string, templateId: string, data: { text: string; sortOrder?: number }) =>
req<ChecklistItem>('POST', `/api/hotels/${slug}/checklist-templates/${templateId}/items`, {
text: data.text,
sort_order: data.sortOrder ?? 0,
}),
updateItem: (slug: string, templateId: string, itemId: string, data: { text?: string; sortOrder?: number }) =>
req<ChecklistItem>('PATCH', `/api/hotels/${slug}/checklist-templates/${templateId}/items/${itemId}`, {
text: data.text,
sort_order: data.sortOrder,
}),
deleteItem: (slug: string, templateId: string, itemId: string) =>
req<void>('DELETE', `/api/hotels/${slug}/checklist-templates/${templateId}/items/${itemId}`),
getTaskChecklist: (slug: string, taskId: string) =>
req<TaskChecklist>('GET', `/api/hotels/${slug}/housekeeping/${taskId}/checklist`),
completeItem: (slug: string, taskId: string, itemId: string) =>
req<{ id: string }>('POST', `/api/hotels/${slug}/housekeeping/${taskId}/checklist/${itemId}/complete`),
uncompleteItem: (slug: string, taskId: string, itemId: string) =>
req<void>('DELETE', `/api/hotels/${slug}/housekeeping/${taskId}/checklist/${itemId}/complete`),
getHousekeepingRules: (slug: string) =>
req<{ requireMinibarCheck: boolean }>('GET', `/api/hotels/${slug}/housekeeping-rules`),
updateHousekeepingRules: (slug: string, data: { requireMinibarCheck: boolean }) =>
req<{ requireMinibarCheck: boolean }>('PATCH', `/api/hotels/${slug}/housekeeping-rules`, {
require_minibar_check: data.requireMinibarCheck,
}),
},
// ── Minibar ───────────────────────────────────────────────────────────────
minibar: {
listItems: (slug: string) =>
req<MinibarItem[]>('GET', `/api/hotels/${slug}/minibar-items`),
createItem: (slug: string, data: { name: string; price: number; category?: string; sortOrder?: number }) =>
req<MinibarItem>('POST', `/api/hotels/${slug}/minibar-items`, {
name: data.name,
price: data.price,
category: data.category,
sort_order: data.sortOrder ?? 0,
}),
updateItem: (slug: string, itemId: string, data: { name?: string; price?: number; category?: string; sortOrder?: number; isActive?: boolean }) =>
req<MinibarItem>('PATCH', `/api/hotels/${slug}/minibar-items/${itemId}`, {
name: data.name,
price: data.price,
category: data.category,
sort_order: data.sortOrder,
is_active: data.isActive,
}),
deleteItem: (slug: string, itemId: string) =>
req<void>('DELETE', `/api/hotels/${slug}/minibar-items/${itemId}`),
getTaskConsumptions: (slug: string, taskId: string) =>
req<MinibarConsumption[]>('GET', `/api/hotels/${slug}/housekeeping/${taskId}/minibar`),
addConsumption: (slug: string, taskId: string, data: { itemId: string; quantity: number }) =>
req<MinibarConsumption>('POST', `/api/hotels/${slug}/housekeeping/${taskId}/minibar`, {
item_id: data.itemId,
quantity: data.quantity,
}),
deleteConsumption: (slug: string, consumptionId: string) =>
req<void>('DELETE', `/api/hotels/${slug}/minibar-consumptions/${consumptionId}`),
getBookingMinibar: (slug: string, bookingId: string) =>
req<MinibarBookingCharge[]>('GET', `/api/hotels/${slug}/bookings/${bookingId}/minibar`),
getStock: (slug: string) =>
req<MinibarStockItem[]>('GET', `/api/hotels/${slug}/minibar-stock`),
getReceipts: (slug: string) =>
req<MinibarReceipt[]>('GET', `/api/hotels/${slug}/minibar-receipts`),
createReceipt: (slug: string, data: { supplier?: string; notes?: string; items: Array<{ itemId: string; quantity: number; costPrice?: number }> }) =>
req<MinibarReceipt>('POST', `/api/hotels/${slug}/minibar-receipts`, data),
getWriteoffs: (slug: string) =>
req<MinibarWriteoff[]>('GET', `/api/hotels/${slug}/minibar-writeoffs`),
createWriteoff: (slug: string, data: { reason: string; notes?: string; items: Array<{ itemId: string; quantity: number }> }) =>
req<MinibarWriteoff>('POST', `/api/hotels/${slug}/minibar-writeoffs`, data),
getInventories: (slug: string) =>
req<MinibarInventoryCheck[]>('GET', `/api/hotels/${slug}/minibar-inventory`),
createInventory: (slug: string, data: { checkedAt?: string; notes?: string }) =>
req<MinibarInventoryCheck>('POST', `/api/hotels/${slug}/minibar-inventory`, data),
getInventory: (slug: string, checkId: string) =>
req<MinibarInventoryCheck>('GET', `/api/hotels/${slug}/minibar-inventory/${checkId}`),
updateInventoryItem: (slug: string, checkId: string, itemId: string, actualQty: number) =>
req<{ ok: boolean }>('PATCH', `/api/hotels/${slug}/minibar-inventory/${checkId}/items/${itemId}`, { actual_qty: actualQty }),
completeInventory: (slug: string, checkId: string) =>
req<{ ok: boolean }>('POST', `/api/hotels/${slug}/minibar-inventory/${checkId}/complete`, {}),
getReport: (slug: string, start?: string, end?: string) =>
req<MinibarReportRow[]>('GET', `/api/hotels/${slug}/minibar-report${start ? `?start=${start}&end=${end ?? ''}` : ''}`),
},
// ── Deposits ──────────────────────────────────────────────────────────────
deposits: {
getSettings: (slug: string) =>
req<DepositSettings>('GET', `/api/hotels/${slug}/deposit/settings`),
updateSettings: (slug: string, data: Partial<DepositSettingsPayload>) =>
req<DepositSettings>('PATCH', `/api/hotels/${slug}/deposit/settings`, data),
getBookingDeposit: (slug: string, bookingId: string) =>
req<BookingDeposit>('GET', `/api/hotels/${slug}/bookings/${bookingId}/deposit`),
payByCash: (slug: string, bookingId: string) =>
req<BookingDeposit>('POST', `/api/hotels/${slug}/bookings/${bookingId}/deposit/cash`),
createYookassaHold: (slug: string, bookingId: string) =>
req<BookingDeposit & { confirmationUrl: string | null }>('POST', `/api/hotels/${slug}/bookings/${bookingId}/deposit/yookassa`),
release: (slug: string, bookingId: string, capturedAmount: number, reason?: string, items?: Array<{ name: string; amount: number }>) =>
req<BookingDeposit>('POST', `/api/hotels/${slug}/bookings/${bookingId}/deposit/release`, {
captured_amount: capturedAmount,
reason,
items,
}),
history: (slug: string) =>
req<BookingDeposit[]>('GET', `/api/hotels/${slug}/deposits`),
cancel: (slug: string, bookingId: string) =>
req<void>('DELETE', `/api/hotels/${slug}/bookings/${bookingId}/deposit`),
refund: (slug: string, bookingId: string, amount?: number, reason?: string) =>
req<BookingDeposit>('POST', `/api/hotels/${slug}/bookings/${bookingId}/deposit/refund`, { amount, reason }),
getMinibarForBooking: (slug: string, bookingId: string) =>
req<{ items: Array<{ itemName: string; quantity: number; recordedPrice: number; currentPrice: number; lineTotal: number }>; total: number }>(
'GET', `/api/hotels/${slug}/bookings/${bookingId}/deposit/minibar`,
),
getPresets: (slug: string) =>
req<DepositPreset[]>('GET', `/api/hotels/${slug}/deposit/presets`),
createPreset: (slug: string, name: string, amount: number) =>
req<DepositPreset>('POST', `/api/hotels/${slug}/deposit/presets`, { name, amount }),
updatePreset: (slug: string, presetId: string, data: { name?: string; amount?: number }) =>
req<DepositPreset>('PATCH', `/api/hotels/${slug}/deposit/presets/${presetId}`, data),
deletePreset: (slug: string, presetId: string) =>
req<void>('DELETE', `/api/hotels/${slug}/deposit/presets/${presetId}`),
},
payments: {
list: (slug: string, bookingId: string) =>
req<BookingPayment[]>('GET', `/api/hotels/${slug}/bookings/${bookingId}/payments`),
add: (slug: string, bookingId: string, amount: number, method: string, note?: string) =>
req<BookingPayment>('POST', `/api/hotels/${slug}/bookings/${bookingId}/payments`, { amount, method, note }),
remove: (slug: string, bookingId: string, paymentId: string) =>
req<void>('DELETE', `/api/hotels/${slug}/bookings/${bookingId}/payments/${paymentId}`),
},
paymentMethods: {
list: (slug: string) =>
req<HotelPaymentMethod[]>('GET', `/api/hotels/${slug}/payment-methods`),
create: (slug: string, data: { name: string; currency?: string; type?: string; sortOrder?: number }) =>
req<HotelPaymentMethod>('POST', `/api/hotels/${slug}/payment-methods`, data),
update: (slug: string, id: string, data: Partial<{ name: string; currency: string; type: string; sortOrder: number; isActive: boolean }>) =>
req<HotelPaymentMethod>('PATCH', `/api/hotels/${slug}/payment-methods/${id}`, data),
remove: (slug: string, id: string) =>
req<void>('DELETE', `/api/hotels/${slug}/payment-methods/${id}`),
getSettings: (slug: string) =>
req<{ requirePaymentCheckin: 'none' | 'soft' | 'hard' }>('GET', `/api/hotels/${slug}/payment-settings`),
updateSettings: (slug: string, requirePaymentCheckin: 'none' | 'soft' | 'hard') =>
req<{ requirePaymentCheckin: string }>('PATCH', `/api/hotels/${slug}/payment-settings`, { require_payment_checkin: requirePaymentCheckin }),
},
paymentGateways: {
list: (slug: string) =>
req<PaymentGateway[]>('GET', `/api/hotels/${slug}/payment-gateways`),
create: (slug: string, data: PaymentGatewayPayload) =>
req<PaymentGateway>('POST', `/api/hotels/${slug}/payment-gateways`, data),
update: (slug: string, id: string, data: Partial<PaymentGatewayPayload>) =>
req<PaymentGateway>('PATCH', `/api/hotels/${slug}/payment-gateways/${id}`, data),
remove: (slug: string, id: string) =>
req<{ ok: boolean }>('DELETE', `/api/hotels/${slug}/payment-gateways/${id}`),
},
// Public widget API (no auth)
widget: {
getConfig: (slug: string) =>
fetch(`${BASE}/api/widget/${slug}/config`).then(r => r.json()) as Promise<WidgetConfig>,
getAvailability: (slug: string, checkIn: string, checkOut: string) =>
fetch(`${BASE}/api/widget/${slug}/availability?checkIn=${checkIn}&checkOut=${checkOut}`).then(r => r.json()) as Promise<WidgetRoom[]>,
createBooking: (slug: string, data: WidgetBookingPayload) =>
fetch(`${BASE}/api/widget/${slug}/bookings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
}).then(async r => {
const json = await r.json()
if (!r.ok) throw { status: r.status, ...json }
return json as { bookingId: string; status: string; confirmationUrl: string | null }
}),
getBookingStatus: (slug: string, bookingId: string) =>
fetch(`${BASE}/api/widget/${slug}/bookings/${bookingId}/status`).then(r => r.json()),
lookupGuest: (slug: string, params: { email?: string; phone?: string }) => {
const qs = new URLSearchParams()
if (params.email) qs.set('email', params.email)
if (params.phone) qs.set('phone', params.phone)
return fetch(`${BASE}/api/widget/${slug}/guests/lookup?${qs}`).then(async r => {
if (!r.ok) return null
return r.json() as Promise<{ first_name: string; last_name: string; middle_name?: string; email?: string; phone?: string; is_blacklisted: boolean }>
})
},
createRentalBooking: (slug: string, data: WidgetRentalBookingPayload) =>
fetch(`${BASE}/api/widget/${slug}/rental-bookings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
}).then(async r => {
const json = await r.json()
if (!r.ok) throw { status: r.status, ...json }
return json as { bookingId: string; status: string }
}),
},
}
// ── Schedule ─────────────────────────────────────────────────────────────────
export interface ScheduleEntry {
id: string
userId: string
date: string
shiftStart: string | null
shiftEnd: string | null
isDayOff: boolean
notes: string | null
userName: string
userRole: string
userPosition: string | null
}
// ── Payload types & converters ───────────────────────────────────────────────
export interface RoomPayload {
number?: string; type?: string; floor?: number
maxGuests?: number; baseRate?: number; status?: string
amenities?: string[]; name?: string; categoryId?: string
bedType?: string; beds?: unknown; housekeepingStatus?: string
sortOrder?: number; allowHourly?: boolean; hourlyRate?: number
extraPlace?: unknown; childPolicy?: unknown
description?: string; photos?: string[]
earlyCheckinFee?: number | null; lateCheckoutFee?: number | null
maintenanceFrom?: string | null; maintenanceTo?: string | null
}
function toRoomPayload(r: Partial<RoomPayload>): Record<string, unknown> {
const out: Record<string, unknown> = {}
if (r.number !== undefined) out.number = r.number
if (r.type !== undefined) out.type = r.type
if (r.floor !== undefined) out.floor = r.floor
if (r.maxGuests !== undefined) out.max_guests = r.maxGuests
if (r.baseRate !== undefined) out.base_rate = r.baseRate
if (r.status !== undefined) out.status = r.status
if (r.amenities !== undefined) out.amenities = r.amenities
if (r.name !== undefined) out.name = r.name
if (r.categoryId !== undefined) out.category_id = r.categoryId
if (r.bedType !== undefined) out.bed_type = r.bedType
if (r.beds !== undefined) out.beds = r.beds
if (r.housekeepingStatus !== undefined) out.housekeeping_status = r.housekeepingStatus
if (r.sortOrder !== undefined) out.sort_order = r.sortOrder
if (r.allowHourly !== undefined) out.allow_hourly = r.allowHourly
if (r.hourlyRate !== undefined) out.hourly_rate = r.hourlyRate
if (r.extraPlace !== undefined) out.extra_place = r.extraPlace
if (r.childPolicy !== undefined) out.child_policy = r.childPolicy
if (r.description !== undefined) out.description = r.description
if (r.photos !== undefined) out.photos = r.photos
if (r.earlyCheckinFee !== undefined) out.early_checkin_fee = r.earlyCheckinFee
if (r.lateCheckoutFee !== undefined) out.late_checkout_fee = r.lateCheckoutFee
if (r.maintenanceFrom !== undefined) out.maintenance_from = r.maintenanceFrom
if (r.maintenanceTo !== undefined) out.maintenance_to = r.maintenanceTo
return out
}
export interface BookingPayload {
roomId?: string; guestName?: string; guestEmail?: string; guestPhone?: string
checkIn?: string; checkOut?: string; adults?: number; children?: number
status?: string; source?: string; totalAmount?: number; paidAmount?: number; notes?: string
tariffId?: string | null
}
function toBookingPayload(b: Partial<BookingPayload>): Record<string, unknown> {
const out: Record<string, unknown> = {}
if (b.roomId !== undefined) out.room_id = b.roomId
if (b.guestName !== undefined) out.guest_name = b.guestName
if (b.guestEmail !== undefined) out.guest_email = b.guestEmail
if (b.guestPhone !== undefined) out.guest_phone = b.guestPhone
if (b.checkIn !== undefined) out.check_in = b.checkIn
if (b.checkOut !== undefined) out.check_out = b.checkOut
if (b.adults !== undefined) out.adults = b.adults
if (b.children !== undefined) out.children = b.children
if (b.status !== undefined) out.status = b.status
if (b.source !== undefined) out.source = b.source
if (b.totalAmount !== undefined) out.total_amount = b.totalAmount
if (b.paidAmount !== undefined) out.paid_amount = b.paidAmount
if (b.notes !== undefined) out.notes = b.notes
if (b.tariffId !== undefined) out.tariff_id = b.tariffId
return out
}
export interface HkPayload {
room_id?: string; type?: string; priority?: string
status?: string; assignee_id?: string; notes?: string; due_date?: string
category?: string; photos?: string[]
resolution_notes?: string; resolution_photos?: string[]
minibar_checked?: boolean
}
export interface HkSettings {
checkout_auto: boolean
checkout_priority: string
inspection_after_clean: boolean
require_completion_photo?: boolean
emergency_close_days?: number
auto_assign?: boolean
auto_assign_strategy?: string
}
export interface HotelPayload {
name?: string; address?: string; phone?: string
timezone?: string; currency?: string
checkInTime?: string; checkOutTime?: string
}
export interface BookingGuest {
id: string
bookingId: string
hotelId: string
guestId: string | null
firstName: string
lastName: string
middleName: string | null
birthDate: string | null
isChild: boolean
isMain: boolean
passportSeries: string | null
passportNumber: string | null
passportIssuedBy: string | null
passportIssueDate: string | null
nationality: string | null
createdAt: string
updatedAt: string
}
export interface BookingGuestPayload {
first_name?: string
last_name?: string
middle_name?: string
birth_date?: string
is_child?: boolean
is_main?: boolean
passport_series?: string
passport_number?: string
passport_issued_by?: string
passport_issue_date?: string
nationality?: string
}
export interface HotelSettings {
require_guest_docs?: boolean
[key: string]: unknown
}
export interface GuestApiType {
id: string
hotelId: string
firstName: string
lastName: string
middleName: string | null
email: string | null
phone: string | null
passport: string | null
passportSeries: string | null
passportNumber: string | null
passportIssuedBy: string | null
passportIssueDate: string | null
birthDate: string | null
nationality: string | null
gender: string | null
city: string | null
notes: string
tags: string[]
loyaltyTier: string
loyaltyPoints: number
rating: number
isBlacklisted: boolean
totalStays: number
totalSpent: number
lastVisit: string | null
createdAt: string
updatedAt: string
history?: {
id: string
checkIn: string
checkOut: string
status: string
totalAmount: number | null
paidAmount: number
roomNumber: string | null
roomType: string | null
}[]
}
export interface GuestPayload {
first_name?: string
last_name?: string
middle_name?: string
email?: string
phone?: string
passport?: string
passport_series?: string
passport_number?: string
birth_date?: string
nationality?: string
gender?: string
city?: string
notes?: string
tags?: string[]
rating?: number
is_blacklisted?: boolean
}
export interface RentalObjectApi {
id: string
hotelId: string
name: string
icon: string
color: string
textColor: string
pricePerHour: number
pricePerDay: number
openHour: number
closeHour: number
maxHoursPerSlot: number | null
bufferMinutes: number
sortOrder: number
isActive: boolean
createdAt: string
}
export interface RentalBookingApi {
id: string
hotelId: string
objectId: string
date: string
isFullDay: boolean
startHour: number
endHour: number
guestName: string
guestPhone: string
linkedRoomId: string | null
totalAmount: number
paidAmount: number
status: 'confirmed' | 'cancelled'
notes: string | null
createdAt: string
}
export interface RentalBookingPayload {
object_id: string
date: string
is_full_day?: boolean
start_hour?: number
end_hour?: number
guest_name: string
guest_phone?: string
linked_room_id?: string
total_amount?: number
paid_amount?: number
status?: string
notes?: string
}
export interface RentalObjectPayload {
name: string
icon?: string
color?: string
text_color?: string
price_per_hour?: number
price_per_day?: number
open_hour?: number
close_hour?: number
max_hours_per_slot?: number | null
buffer_minutes?: number
sort_order?: number
is_active?: boolean
}
export interface CategoryApi {
id: string
hotel_id: string
name: string
description: string
color: string
amenities: string[]
photos: string[]
sort_order: number
base_price: number
allow_hourly: boolean
hourly_base_price: number
created_at: string
}
export interface CategoryPayload {
name: string
description?: string
color?: string
amenities?: string[]
photos?: string[]
sort_order?: number
base_price?: number
allow_hourly?: boolean
hourly_base_price?: number
}
export interface TariffApi {
id: string
hotel_id: string
name: string
code: string
meal_plan: string
inclusions: string[]
modifier_type: 'fixed' | 'percent'
modifier_value: number
min_nights: number
cancellation_policy: 'flexible' | 'moderate' | 'strict' | 'nonrefundable'
description: string
is_active: boolean
created_at: string
}
export interface TariffPayload {
name: string
code: string
meal_plan?: string
inclusions?: string[]
modifier_type?: string
modifier_value?: number
min_nights?: number
cancellation_policy?: string
description?: string
is_active?: boolean
}
export interface RateOverrideApi {
categoryId: string
date: string
price: number
extraPerson: number
minNights: number
channelPrices: Record<string, number>
closed: boolean
hourlyPrice?: number | null
}
export interface RateOverridePayload {
category_id: string
date: string
price: number
extra_person?: number
min_nights?: number
channel_prices?: Record<string, number>
closed?: boolean
hourly_price?: number | null
}
export interface RatePeriodApi {
id: string
name: string
startDate: string
endDate: string
notes: string | null
categoryPrices: Record<string, number>
channelMarkup: Record<string, number>
extraPersonPrice: number
minNights: number
daysOfWeek: number[] | null
}
export interface RatePeriodPayload {
name: string
start_date: string
end_date: string
notes?: string
category_prices?: Record<string, number>
channel_markup?: Record<string, number>
extra_person_price?: number
min_nights?: number
days_of_week?: number[] | null
}
export interface LoyaltySettings {
isActive: boolean
pointsPerRuble: number
pointValue: number
expiryMonths: number
}
export interface LoyaltyGuestApi {
id: string
name: string
email: string | null
phone: string | null
loyaltyPoints: number
loyaltyTier: string
totalSpent: number
}
export interface LoyaltyTransaction {
id: string
guestId: string
guestName: string
amount: number
reason: string
notes: string | null
staffName: string | null
createdAt: string
}
export interface ChatRoom {
id: string
type: 'general' | 'direct' | 'notifications'
name: string | null
unreadCount: number
lastMessage: string | null
lastMessageAt: string | null
lastSender: string | null
otherUserName: string | null
otherUserId: string | null
otherUserLastRead: string | null
}
export interface ChatReaction {
emoji: string
count: number
hasOwn: boolean
}
export interface ChatMessage {
id: string
roomId: string
senderId: string | null
senderName: string
senderRole: string
text: string
createdAt: string
editedAt: string | null
deletedAt: string | null
isSystem: boolean
systemName: string | null
attachmentUrl: string | null
reactions: ChatReaction[]
}
export interface ChatSearchResult {
id: string
roomId: string
text: string
createdAt: string
senderName: string
roomType: 'general' | 'direct' | 'notifications'
roomName: string | null
otherUserName: string | null
}
export interface WorkstationDevice {
id: string
workstationId: string
type: 'kkt' | 'printer' | 'netup'
name: string
connection: 'usb' | 'network' | 'com' | 'windows'
networkHost?: string
networkPort?: number
comPort?: string
purpose: 'fiscal' | 'kitchen' | 'bar' | 'receipt' | 'other'
config: Record<string, unknown> & { dto_user?: string; dto_pass?: string; dto_device_id?: string }
fiscalMode?: boolean
dtoPort?: number
}
export interface Workstation {
id: string
hotelId: string
agentId: string | null
name: string
hostname: string | null
ipAddress: string | null
isOnline: boolean
lastSeen: string | null
agentVersion?: string
ttlockComPort?: string | null
createdAt: string
devices: WorkstationDevice[] | null
}
export interface TTLockConfig {
isEnabled: boolean
clientId: string
cardSectors: string
apiServer: string
}
export interface TTLockConfigUpdate {
isEnabled?: boolean
clientId?: string
clientSecret?: string
cardSectors?: string
apiServer?: string
}
export interface RoomLockMapping {
roomId: string
roomName: string
roomNumber: string
lockMac: string
lockName: string | null
buildNo: number
floorNo: number
}
export interface CardIssuanceResult {
ok: boolean
issuanceId: string
issuedAt: string
roomName: string
roomNumber: string
lockMac: string
lockName: string | null
checkIn: string
checkOut: string
cardNumber: string | null
}
export interface CardIssuance {
id: string
lockMac: string
issuedAt: string
issuedBy: string | null
checkIn: string
checkOut: string
isRevoked: boolean
revokedAt: string | null
}
export interface WifiSettings {
id: string
hotelId: string
enabled: boolean
apiToken: string
authMethod: 'room_lastname' | 'room_birthdate' | 'room_any' | 'room_only'
ssidName: string | null
welcomeText: string | null
sessionHours: number
}
// ── Checklist types ──────────────────────────────────────────────────────────
export interface ChecklistItem {
id: string
templateId: string
text: string
sortOrder: number
}
export interface ChecklistTemplate {
id: string
hotelId: string
name: string
taskType: string | null
isActive: boolean
requireBeforeComplete: boolean
sortOrder: number
createdAt: string
items: ChecklistItem[]
}
export interface TaskChecklistItem {
id: string
text: string
sortOrder: number
completedAt: string | null
completedBy: string | null
}
export interface TaskChecklist {
templateId: string | null
templateName: string | null
items: TaskChecklistItem[]
}
// ── Minibar types ─────────────────────────────────────────────────────────────
export interface MinibarItem {
id: string
hotelId: string
name: string
price: number
category: string | null
isActive: boolean
sortOrder: number
}
export interface MinibarConsumption {
id: string
hotelId: string
roomId: string
bookingId: string | null
taskId: string | null
itemId: string
itemName: string
quantity: number
pricePerUnit: number
recordedByName: string | null
recordedAt: string
}
export interface MinibarBookingCharge {
id: string
itemName: string
quantity: number
pricePerUnit: number
currentPrice: number
total: number
recordedAt: string
recordedByName: string | null
}
export interface MinibarStockItem extends MinibarItem {
stockQty: number
}
export interface MinibarReceiptItem {
id: string
itemId: string
itemName: string
quantity: number
costPrice: number | null
}
export interface MinibarReceipt {
id: string
supplier: string | null
notes: string | null
createdAt: string
items: MinibarReceiptItem[]
}
export interface MinibarWriteoffItem {
id: string
itemId: string
itemName: string
quantity: number
}
export interface MinibarWriteoff {
id: string
reason: string
notes: string | null
createdAt: string
items: MinibarWriteoffItem[]
}
export interface MinibarInventoryItem {
id: string
itemId: string
itemName: string
category: string | null
expectedQty: number
actualQty: number
}
export interface MinibarInventoryCheck {
id: string
checkedAt: string
notes: string | null
isComplete: boolean
createdAt: string
items?: MinibarInventoryItem[]
}
export interface HotelPaymentMethod {
id: string
hotelId: string
name: string
currency: string
type: 'cash' | 'card' | 'transfer' | 'other'
isActive: boolean
sortOrder: number
}
export interface MinibarReportRow {
name: string
category: string | null
totalQty: number
totalRevenue: number
}
// ── Deposit types ─────────────────────────────────────────────────────────────
export interface DepositPreset {
id: string
hotelId: string
name: string
amount: number
sortOrder: number
createdAt: string
}
export interface BookingPayment {
id: string
hotelId: string
bookingId: string
amount: number
method: string
note: string | null
createdAt: string
createdByName: string | null
}
export interface DepositSettings {
hotelId: string
isEnabled: boolean
amount: number
yookassaShopId: string | null
yookassaSecretKey: string | null
releaseRequiresCheckout: boolean
longStayFullPayment: boolean
longStayThresholdDays: number
updatedAt?: string
}
export interface DepositSettingsPayload {
is_enabled?: boolean
amount?: number
yookassa_shop_id?: string
yookassa_secret_key?: string
release_requires_checkout?: boolean
long_stay_full_payment?: boolean
long_stay_threshold_days?: number
}
export interface BookingDeposit {
id: string
hotelId: string
bookingId: string
amount: number
status: string
paymentMethod: string | null
yookassaPaymentId: string | null
yookassaConfirmationUrl: string | null
capturedAmount: number | null
refundedAmount: number | null
retentionReason: string | null
guestEmailSent: boolean
cardLast4: string | null
cardBrand: string | null
createdAt: string
paidAt: string | null
releasedAt: string | null
// history enrichment
guestName?: string
guestEmail?: string
checkIn?: string
checkOut?: string
roomNumber?: string
}
export interface PaymentGateway {
id: string
provider: string
label: string
shopId: string | null
secretKey: string | null
currency: string
isActive: boolean
modules: string[]
autoConfirmOnPayment: boolean
createdAt: string
}
export interface PaymentGatewayPayload {
provider?: string
label: string
shopId?: string
secretKey?: string
currency?: string
isActive?: boolean
modules?: string[]
autoConfirmOnPayment?: boolean
}
export interface WidgetRoom {
id: string
number: string
name: string
type: string
floor: number
maxGuests: number
baseRate: number
amenities: string[]
description: string
photos: string[]
categoryId: string | null
}
export interface WidgetCategory {
id: string
name: string
description: string
amenities: string[]
photos: string[]
minPrice: number
maxGuests: number
}
export interface WidgetRentalObject {
id: string
name: string
icon: string
pricePerHour: number
pricePerDay: number
openHour: number
closeHour: number
maxHoursPerSlot: number | null
bufferMinutes: number
}
export interface WidgetRentalBookingPayload {
objectId: string
date: string
isFullDay?: boolean
startHour?: number
endHour?: number
guestName: string
guestEmail?: string
guestPhone?: string
totalAmount: number
notes?: string
}
export interface WidgetConfig {
hotelId: string
hotelName: string
slug: string
paymentEnabled: boolean
currency: string
rooms: WidgetRoom[]
categories: WidgetCategory[]
rentalObjects?: WidgetRentalObject[]
widgetSettings?: {
primaryColor: string
language: string
roomDisplayMode: string
minNights: number
showRental: boolean
showPromo: boolean
allowExtraBeds: boolean
allowChildren: boolean
hotelName: string
services?: { id: string; enabled: boolean }[]
}
}
export interface WidgetBookingPayload {
roomId?: string
categoryId?: string
checkIn: string
checkOut: string
guestName: string
guestEmail?: string
guestPhone?: string
adults?: number
children?: number
totalAmount: number
notes?: string
services?: Array<{ name: string; price: number }>
}
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
}