Files
hotelsync/src/lib/api.ts
HotelSync a4fc4a0972 feat: deposit — history page, card info, cancel button, webhook notice, online booking email
- Migration 060: card_last4, card_brand columns in booking_deposits
- Backend: webhook saves card brand/last4 from YooKassa notification
- Backend: GET /api/hotels/:slug/deposits — payment history with guest/room info
- Backend: DELETE /api/hotels/:slug/bookings/:bookingId/deposit — cancel hold_created or paid_cash deposit
- Backend: auto-create YooKassa hold + send deposit email for source='online' bookings
- Frontend: DepositHistoryPage with filters, card display, retention reasons
- Frontend: deposit cancel button for hold_created and paid_cash states
- Frontend: show card last4/brand in hold_confirmed status
- Frontend: release form presets (full return / hold all) + improved UX
- Frontend: webhook setup instructions in DepositSettingsPage
- Frontend: "История платежей" link in deposit settings header

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 11:47:51 +03:00

1413 lines
54 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) =>
req<ChatMessage[]>('GET', `/api/hotels/${slug}/chat/rooms/${roomId}/messages?limit=${limit}`),
sendMessage: (slug: string, roomId: string, text: string) =>
req<ChatMessage>('POST', `/api/hotels/${slug}/chat/rooms/${roomId}/messages`, { text }),
markRead: (slug: string, roomId: string) =>
req<{ ok: boolean }>('PATCH', `/api/hotels/${slug}/chat/rooms/${roomId}/read`),
openDirect: (slug: string, otherUserId: string) =>
req<{ room_id: string }>('POST', `/api/hotels/${slug}/chat/direct/${otherUserId}`),
},
// ── 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`),
},
// ── 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) =>
req<BookingDeposit>('POST', `/api/hotels/${slug}/bookings/${bookingId}/deposit/release`, {
captured_amount: capturedAmount,
reason,
}),
history: (slug: string) =>
req<BookingDeposit[]>('GET', `/api/hotels/${slug}/deposits`),
cancel: (slug: string, bookingId: string) =>
req<void>('DELETE', `/api/hotels/${slug}/bookings/${bookingId}/deposit`),
},
}
// ── 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
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
}
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'
name: string | null
unreadCount: number
lastMessage: string | null
lastMessageAt: string | null
lastSender: string | null
otherUserName: string | null
otherUserId: string | null
}
export interface ChatMessage {
id: string
roomId: string
senderId: string
senderName: string
senderRole: string
text: string
createdAt: string
}
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
total: number
recordedAt: string
recordedByName: string | null
}
// ── Deposit types ─────────────────────────────────────────────────────────────
export interface DepositSettings {
hotelId: string
isEnabled: boolean
amount: number
yookassaShopId: string | null
yookassaSecretKey: string | null
updatedAt?: string
}
export interface DepositSettingsPayload {
is_enabled?: boolean
amount?: number
yookassa_shop_id?: string
yookassa_secret_key?: string
}
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
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
}
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
}