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 { constructor(public status: number, message: string) { super(message) } } // ── 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 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 } // ── 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(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 msg = (data as Record)?.error ?? 'Request failed' throw new ApiError(res.status, msg) } 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 }) => req<{ ok: boolean; message: string }>('POST', '/api/auth/register', data), 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('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}`), }, // ── 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) => req('GET', `/api/hotels/${slug}/chat/rooms/${roomId}/messages?limit=${limit}`), sendMessage: (slug: string, roomId: string, text: string) => req('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('GET', `/api/hotels/${slug}/workstations`), create: (slug: string, name: string) => req('POST', `/api/hotels/${slug}/workstations`, { name }), update: (slug: string, id: string, name: string) => req('PATCH', `/api/hotels/${slug}/workstations/${id}`, { name }), 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}`), }, // ── 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 }>( '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 }) => 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 }), }, } // ── 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 } 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 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[] } 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 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 } 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' 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' name: string connection: 'usb' | 'network' networkHost?: string networkPort?: number purpose: 'fiscal' | 'kitchen' | 'bar' | 'receipt' | 'other' config: Record } export interface Workstation { id: string hotelId: string agentId: string | null name: string hostname: string | null ipAddress: string | null isOnline: boolean lastSeen: string | null createdAt: string devices: WorkstationDevice[] | null } 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 }