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 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 = {} for (const [k, v] of Object.entries(val as Record)) { 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 = {} for (const [k, v] of Object.entries(val as Record)) { result[toSnake(k)] = transformKeysToSnake(v) } return result } return val } // ── Core request ───────────────────────────────────────────────────────────── async function req( method: string, path: string, body?: unknown, ): Promise { const headers: Record = {} 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 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 = { 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): 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('POST', '/api/auth/logout'), me: () => req('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 }), acceptInvite: (token: string, password: string) => req<{ access_token: string; user: import('../types').User }>( 'POST', '/api/auth/accept-invite', { token, password }), resendConfirmation: (email: string) => req<{ ok: boolean }>('POST', '/api/auth/resend-confirmation', { email }), }, // ── Rooms ───────────────────────────────────────────────────────────────── rooms: { list: (slug: string) => req('GET', `/api/hotels/${slug}/rooms`).then(rooms => rooms.map(normalizeRoom)), create: (slug: string, data: RoomPayload) => req('POST', `/api/hotels/${slug}/rooms`, toRoomPayload(data)).then(normalizeRoom), update: (slug: string, id: string, data: Partial) => req('PATCH', `/api/hotels/${slug}/rooms/${id}`, toRoomPayload(data)).then(normalizeRoom), delete: (slug: string, id: string) => req('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('GET', `/api/hotels/${slug}/bookings${q ? `?${q}` : ''}`) }, create: (slug: string, data: BookingPayload) => req('POST', `/api/hotels/${slug}/bookings`, toBookingPayload(data)), update: (slug: string, id: string, data: Partial) => req('PATCH', `/api/hotels/${slug}/bookings/${id}`, toBookingPayload(data)), delete: (slug: string, id: string) => req('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('GET', `/api/hotels/${slug}/housekeeping${q ? `?${q}` : ''}`) }, create: (slug: string, data: HkPayload) => req('POST', `/api/hotels/${slug}/housekeeping`, data), update: (slug: string, id: string, data: Partial) => req('PATCH', `/api/hotels/${slug}/housekeeping/${id}`, data), delete: (slug: string, id: string) => req('DELETE', `/api/hotels/${slug}/housekeeping/${id}`), getSettings: (slug: string) => req('GET', `/api/hotels/${slug}/housekeeping-settings`), saveSettings: (slug: string, data: Partial) => req('PATCH', `/api/hotels/${slug}/housekeeping-settings`, data), }, // ── Channels ────────────────────────────────────────────────────────────── channels: { list: async (slug: string): Promise => { const raw = await req[]>('GET', `/api/hotels/${slug}/channels`) return raw.map(normalizeChannel) }, update: async (slug: string, id: string, data: { enabled?: boolean; api_key?: string }): Promise => { const raw = await req>('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>('POST', `/api/hotels/${slug}/channels/${id}/sync`) return { channel: normalizeChannel(raw), syncedBookings: (raw.syncedBookings as number) ?? 0 } }, }, // ── Users ───────────────────────────────────────────────────────────────── users: { list: (slug: string) => req('GET', `/api/hotels/${slug}/users`), create: (slug: string, data: { email: string; password: string; name: string; role: string; phone?: string; position?: string }) => req('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('PATCH', `/api/hotels/${slug}/users/${id}`, data), delete: (slug: string, id: string) => req('DELETE', `/api/hotels/${slug}/users/${id}`), }, // ── Role Permissions ────────────────────────────────────────────────────── rolePermissions: { list: (slug: string) => req( 'GET', `/api/hotels/${slug}/role-permissions`), save: (slug: string, roleKey: string, data: { name: string; color: string; isSystem: boolean; permissions: Record; homePage?: string | null }) => req( 'PUT', `/api/hotels/${slug}/role-permissions/${roleKey}`, data), delete: (slug: string, roleKey: string) => req('DELETE', `/api/hotels/${slug}/role-permissions/${roleKey}`), }, // ── Schedule ────────────────────────────────────────────────────────────── schedule: { list: (slug: string, from: string, to: string) => req('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('PUT', `/api/hotels/${slug}/schedule`, data), remove: (slug: string, userId: string, date: string) => req('DELETE', `/api/hotels/${slug}/schedule/${userId}/${date}`), }, // ── Loyalty ─────────────────────────────────────────────────────────────── loyalty: { getSettings: (slug: string) => req('GET', `/api/hotels/${slug}/loyalty/settings`), saveSettings: (slug: string, data: Partial) => req('PATCH', `/api/hotels/${slug}/loyalty/settings`, data), listGuests: (slug: string, q?: string) => req('GET', `/api/hotels/${slug}/loyalty/guests${q ? `?q=${encodeURIComponent(q)}` : ''}`), addPoints: (slug: string, guestId: string, data: { amount: number; reason: string; notes?: string }) => req('POST', `/api/hotels/${slug}/loyalty/guests/${guestId}/points`, data), listTransactions: (slug: string, limit = 50) => req('GET', `/api/hotels/${slug}/loyalty/transactions?limit=${limit}`), }, // ── Chat ────────────────────────────────────────────────────────────────── chat: { listRooms: (slug: string) => req('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('GET', `/api/hotels/${slug}/chat/rooms/${roomId}/messages${qs}`) }, sendMessage: (slug: string, roomId: string, text: string, attachmentUrl?: string) => req('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}`), listMembers: (slug: string) => req<{ id: string; name: string; role: string }[]>('GET', `/api/hotels/${slug}/chat/members`), createGroup: (slug: string, name: string, memberIds: string[]) => req<{ roomId: string }>('POST', `/api/hotels/${slug}/chat/group`, { name, memberIds }), updateGroup: (slug: string, roomId: string, patch: { name?: string; avatarUrl?: string; addMemberIds?: string[]; removeMemberIds?: string[]; transferOwnerTo?: string }) => req<{ ok: boolean }>('PATCH', `/api/hotels/${slug}/chat/rooms/${roomId}/group`, patch), editMessage: (slug: string, roomId: string, msgId: string, text: string) => req('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('POST', `/api/hotels/${slug}/chat/rooms/${roomId}/messages/${msgId}/reactions`, { emoji }), search: (slug: string, q: string) => req('GET', `/api/hotels/${slug}/chat/search?q=${encodeURIComponent(q)}`), notify: (slug: string, text: string, systemName?: string) => req('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('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('GET', `/api/hotels/${slug}/chat/rooms/${roomId}/typing`), }, // ── Workstations ────────────────────────────────────────────────────────── workstations: { list: (slug: string) => req('GET', `/api/hotels/${slug}/workstations`), create: (slug: string, name: string) => req('POST', `/api/hotels/${slug}/workstations`, { name }), update: (slug: string, id: string, data: { name?: string; ttlockComPort?: string | null }) => req('PATCH', `/api/hotels/${slug}/workstations/${id}`, { name: data.name, ttlock_com_port: data.ttlockComPort, }), remove: (slug: string, id: string) => req('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('GET', `/api/hotels/${slug}/workstations/${id}/devices`), addDevice: (slug: string, id: string, data: Partial) => req('POST', `/api/hotels/${slug}/workstations/${id}/devices`, data), updateDevice: (slug: string, id: string, deviceId: string, data: Partial) => req('PATCH', `/api/hotels/${slug}/workstations/${id}/devices/${deviceId}`, data), removeDevice: (slug: string, id: string, deviceId: string) => req('DELETE', `/api/hotels/${slug}/workstations/${id}/devices/${deviceId}`), testDevice: (slug: string, wsId: string, deviceId: string) => req<{ ok: boolean; error?: string; message?: string; kkt?: Record; shift?: Record }>('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('GET', `/api/hotels/${slug}`), update: (slug: string, data: HotelPayload) => req('PATCH', `/api/hotels/${slug}`, toHotelPayload(data)), }, // ── Booking Guests ──────────────────────────────────────────────────────── bookingGuests: { list: (slug: string, bookingId: string) => req('GET', `/api/hotels/${slug}/bookings/${bookingId}/guests`), add: (slug: string, bookingId: string, data: BookingGuestPayload) => req('POST', `/api/hotels/${slug}/bookings/${bookingId}/guests`, data), update: (slug: string, bookingId: string, id: string, data: BookingGuestPayload) => req('PATCH', `/api/hotels/${slug}/bookings/${bookingId}/guests/${id}`, data), remove: (slug: string, bookingId: string, id: string) => req('DELETE', `/api/hotels/${slug}/bookings/${bookingId}/guests/${id}`), }, // ── Hotel Settings ──────────────────────────────────────────────────────── hotelSettings: { get: (slug: string) => req('GET', `/api/hotels/${slug}/hotel-settings`), update: (slug: string, data: Partial) => 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('GET', `/api/hotels/${slug}/guests${params}`) }, get: (slug: string, id: string) => req('GET', `/api/hotels/${slug}/guests/${id}`), create: (slug: string, data: GuestPayload) => req('POST', `/api/hotels/${slug}/guests`, data), update: (slug: string, id: string, data: Partial) => req('PATCH', `/api/hotels/${slug}/guests/${id}`, data), delete: (slug: string, id: string) => req('DELETE', `/api/hotels/${slug}/guests/${id}`), }, // ── Rental ──────────────────────────────────────────────────────────────── rental: { listObjects: (slug: string) => req('GET', `/api/hotels/${slug}/rental-objects`), createObject: (slug: string, data: RentalObjectPayload) => req('POST', `/api/hotels/${slug}/rental-objects`, data), updateObject: (slug: string, id: string, data: Partial) => req('PATCH', `/api/hotels/${slug}/rental-objects/${id}`, data), deleteObject: (slug: string, id: string) => req('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('GET', `/api/hotels/${slug}/rental-bookings${q ? `?${q}` : ''}`) }, createBooking: (slug: string, data: RentalBookingPayload) => req('POST', `/api/hotels/${slug}/rental-bookings`, data), updateBooking: (slug: string, id: string, data: Partial) => req('PATCH', `/api/hotels/${slug}/rental-bookings/${id}`, data), deleteBooking: (slug: string, id: string) => req('DELETE', `/api/hotels/${slug}/rental-bookings/${id}`), }, // ── File Upload ─────────────────────────────────────────────────────────── upload: { photo: async (file: File, folder: 'categories' | 'rooms' | 'hotels' | 'guests' | 'tasks' = 'rooms'): Promise => { 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 => { await req('DELETE', `/api/upload?url=${encodeURIComponent(url)}`) }, }, // ── Categories ──────────────────────────────────────────────────────────── categories: { list: (slug: string) => req('GET', `/api/hotels/${slug}/categories`), create: (slug: string, data: CategoryPayload) => req('POST', `/api/hotels/${slug}/categories`, data), update: (slug: string, id: string, data: Partial) => req('PATCH', `/api/hotels/${slug}/categories/${id}`, data), delete: (slug: string, id: string) => req('DELETE', `/api/hotels/${slug}/categories/${id}`), }, // ── Tariffs ─────────────────────────────────────────────────────────────── tariffs: { list: (slug: string) => req('GET', `/api/hotels/${slug}/tariffs`), create: (slug: string, data: TariffPayload) => req('POST', `/api/hotels/${slug}/tariffs`, data), update: (slug: string, id: string, data: Partial) => req('PATCH', `/api/hotels/${slug}/tariffs/${id}`, data), delete: (slug: string, id: string) => req('DELETE', `/api/hotels/${slug}/tariffs/${id}`), }, // ── Rate Overrides (per-cell prices) ───────────────────────────────────── rateOverrides: { list: (slug: string) => req('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[]>('GET', `/api/hotels/${slug}/notifications`), markRead: (slug: string, id: string) => req>('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('DELETE', `/api/hotels/${slug}/notifications/${id}`), }, // ── Rate Periods ───────────────────────────────────────────────────────── ratePeriods: { list: (slug: string) => req('GET', `/api/hotels/${slug}/rate-periods`), create: (slug: string, data: RatePeriodPayload) => req('POST', `/api/hotels/${slug}/rate-periods`, data), update: (slug: string, id: string, data: Partial) => req('PATCH', `/api/hotels/${slug}/rate-periods/${id}`, data), delete: (slug: string, id: string) => req('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; query: Record; 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('GET', `/api/hotels/${slug}/wifi-settings`), update: (slug: string, data: Partial>) => req('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('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('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('POST', `/api/hotels/${slug}/bookings/${bookingId}/issue-card`, workstationId ? { workstationId } : undefined), getCards: (slug: string, bookingId: string) => req('GET', `/api/hotels/${slug}/bookings/${bookingId}/cards`), }, // ── Checklists ──────────────────────────────────────────────────────────── checklists: { listTemplates: (slug: string) => req('GET', `/api/hotels/${slug}/checklist-templates`), createTemplate: (slug: string, data: { name: string; taskType?: string }) => req('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('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('DELETE', `/api/hotels/${slug}/checklist-templates/${templateId}`), addItem: (slug: string, templateId: string, data: { text: string; sortOrder?: number }) => req('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('PATCH', `/api/hotels/${slug}/checklist-templates/${templateId}/items/${itemId}`, { text: data.text, sort_order: data.sortOrder, }), deleteItem: (slug: string, templateId: string, itemId: string) => req('DELETE', `/api/hotels/${slug}/checklist-templates/${templateId}/items/${itemId}`), getTaskChecklist: (slug: string, taskId: string) => req('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('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('GET', `/api/hotels/${slug}/minibar-items`), createItem: (slug: string, data: { name: string; price: number; category?: string; sortOrder?: number }) => req('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('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('DELETE', `/api/hotels/${slug}/minibar-items/${itemId}`), getTaskConsumptions: (slug: string, taskId: string) => req('GET', `/api/hotels/${slug}/housekeeping/${taskId}/minibar`), addConsumption: (slug: string, taskId: string, data: { itemId: string; quantity: number }) => req('POST', `/api/hotels/${slug}/housekeeping/${taskId}/minibar`, { item_id: data.itemId, quantity: data.quantity, }), deleteConsumption: (slug: string, consumptionId: string) => req('DELETE', `/api/hotels/${slug}/minibar-consumptions/${consumptionId}`), getBookingMinibar: (slug: string, bookingId: string) => req('GET', `/api/hotels/${slug}/bookings/${bookingId}/minibar`), getStock: (slug: string) => req('GET', `/api/hotels/${slug}/minibar-stock`), getReceipts: (slug: string) => req('GET', `/api/hotels/${slug}/minibar-receipts`), createReceipt: (slug: string, data: { supplier?: string; notes?: string; items: Array<{ itemId: string; quantity: number; costPrice?: number }> }) => req('POST', `/api/hotels/${slug}/minibar-receipts`, data), getWriteoffs: (slug: string) => req('GET', `/api/hotels/${slug}/minibar-writeoffs`), createWriteoff: (slug: string, data: { reason: string; notes?: string; items: Array<{ itemId: string; quantity: number }> }) => req('POST', `/api/hotels/${slug}/minibar-writeoffs`, data), getInventories: (slug: string) => req('GET', `/api/hotels/${slug}/minibar-inventory`), createInventory: (slug: string, data: { checkedAt?: string; notes?: string }) => req('POST', `/api/hotels/${slug}/minibar-inventory`, data), getInventory: (slug: string, checkId: string) => req('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('GET', `/api/hotels/${slug}/minibar-report${start ? `?start=${start}&end=${end ?? ''}` : ''}`), }, // ── Deposits ────────────────────────────────────────────────────────────── deposits: { getSettings: (slug: string) => req('GET', `/api/hotels/${slug}/deposit/settings`), updateSettings: (slug: string, data: Partial) => req('PATCH', `/api/hotels/${slug}/deposit/settings`, data), getBookingDeposit: (slug: string, bookingId: string) => req('GET', `/api/hotels/${slug}/bookings/${bookingId}/deposit`), payByCash: (slug: string, bookingId: string) => req('POST', `/api/hotels/${slug}/bookings/${bookingId}/deposit/cash`), createYookassaHold: (slug: string, bookingId: string) => req('POST', `/api/hotels/${slug}/bookings/${bookingId}/deposit/yookassa`), release: (slug: string, bookingId: string, capturedAmount: number, reason?: string, items?: Array<{ name: string; amount: number }>) => req('POST', `/api/hotels/${slug}/bookings/${bookingId}/deposit/release`, { captured_amount: capturedAmount, reason, items, }), history: (slug: string) => req('GET', `/api/hotels/${slug}/deposits`), cancel: (slug: string, bookingId: string) => req('DELETE', `/api/hotels/${slug}/bookings/${bookingId}/deposit`), refund: (slug: string, bookingId: string, amount?: number, reason?: string) => req('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('GET', `/api/hotels/${slug}/deposit/presets`), createPreset: (slug: string, name: string, amount: number) => req('POST', `/api/hotels/${slug}/deposit/presets`, { name, amount }), updatePreset: (slug: string, presetId: string, data: { name?: string; amount?: number }) => req('PATCH', `/api/hotels/${slug}/deposit/presets/${presetId}`, data), deletePreset: (slug: string, presetId: string) => req('DELETE', `/api/hotels/${slug}/deposit/presets/${presetId}`), }, payments: { list: (slug: string, bookingId: string) => req('GET', `/api/hotels/${slug}/bookings/${bookingId}/payments`), add: (slug: string, bookingId: string, amount: number, method: string, note?: string) => req('POST', `/api/hotels/${slug}/bookings/${bookingId}/payments`, { amount, method, note }), remove: (slug: string, bookingId: string, paymentId: string) => req('DELETE', `/api/hotels/${slug}/bookings/${bookingId}/payments/${paymentId}`), }, paymentMethods: { list: (slug: string) => req('GET', `/api/hotels/${slug}/payment-methods`), create: (slug: string, data: { name: string; currency?: string; type?: string; sortOrder?: number }) => req('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('PATCH', `/api/hotels/${slug}/payment-methods/${id}`, data), remove: (slug: string, id: string) => req('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('GET', `/api/hotels/${slug}/payment-gateways`), create: (slug: string, data: PaymentGatewayPayload) => req('POST', `/api/hotels/${slug}/payment-gateways`, data), update: (slug: string, id: string, data: Partial) => req('PATCH', `/api/hotels/${slug}/payment-gateways/${id}`, data), remove: (slug: string, id: string) => req<{ ok: boolean }>('DELETE', `/api/hotels/${slug}/payment-gateways/${id}`), }, // ── Push notifications ──────────────────────────────────────────────────── push: { getVapidKey: () => req<{ publicKey: string | null }>('GET', '/api/push/vapid-key'), subscribe: (sub: { endpoint: string; p256dh: string; auth: string }) => req<{ ok: boolean }>('POST', '/api/push/subscribe', sub), unsubscribe: (endpoint: string) => req<{ ok: boolean }>('DELETE', '/api/push/unsubscribe', { endpoint }), }, // Public widget API (no auth) widget: { getConfig: (slug: string) => fetch(`${BASE}/api/widget/${slug}/config`).then(r => r.json()) as Promise, getAvailability: (slug: string, checkIn: string, checkOut: string) => fetch(`${BASE}/api/widget/${slug}/availability?checkIn=${checkIn}&checkOut=${checkOut}`).then(r => r.json()) as Promise, 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): Record { const out: Record = {} 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): Record { const out: Record = {} 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 closed: boolean hourlyPrice?: number | null } export interface RateOverridePayload { category_id: string date: string price: number extra_person?: number min_nights?: number channel_prices?: Record closed?: boolean hourly_price?: number | null } export interface RatePeriodApi { id: string name: string startDate: string endDate: string notes: string | null categoryPrices: Record channelMarkup: Record extraPersonPrice: number minNights: number daysOfWeek: number[] | null } export interface RatePeriodPayload { name: string start_date: string end_date: string notes?: string category_prices?: Record channel_markup?: Record 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' | 'group' name: string | null unreadCount: number lastMessage: string | null lastMessageAt: string | null lastSender: string | null otherUserName: string | null otherUserRole: string | null otherUserId: string | null otherUserLastRead: string | null memberCount: number memberNames: string[] | null avatarUrl: string | null createdBy?: 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 & { 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 { const out: Record = {} 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 }