Files
hotelsync/src/lib/api.ts

801 lines
30 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 {
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<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 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
}
// ── 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(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<string, string>)?.error ?? 'Request failed'
throw new ApiError(res.status, msg)
}
return transformKeys(data) as T
}
// ── Channel normalization ────────────────────────────────────────────────────
const CHANNEL_DISPLAY_NAMES: Record<string, string> = {
booking_com: 'Booking.com',
airbnb: 'Airbnb',
expedia: 'Expedia',
vrbo: 'VRBO',
yandex_travel: 'Яндекс Путешествия',
ostrovok: 'Островок',
sutochno: 'Суточно.ру',
onetwotrip: 'OneTwoTrip',
}
function normalizeChannel(raw: Record<string, unknown>): Channel {
const name = raw.name as ChannelName
return {
id: raw.id as string,
hotelId: (raw.hotelId as string) ?? '',
name,
displayName: CHANNEL_DISPLAY_NAMES[name as string] ?? String(name),
isEnabled: Boolean(raw.enabled),
lastSyncAt: (raw.lastSyncedAt as string | null) ?? null,
lastSyncStatus: raw.lastSyncedAt ? 'success' : 'idle',
bookingsImported: 0,
mappings: [],
}
}
// ── API methods ──────────────────────────────────────────────────────────────
export const api = {
// ── 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
}) =>
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<Room[]>('GET', `/api/hotels/${slug}/rooms`),
create: (slug: string, data: RoomPayload) =>
req<Room>('POST', `/api/hotels/${slug}/rooms`, toRoomPayload(data)),
update: (slug: string, id: string, data: Partial<RoomPayload>) =>
req<Room>('PATCH', `/api/hotels/${slug}/rooms/${id}`, toRoomPayload(data)),
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 }) => {
const qs = new URLSearchParams()
if (params?.date) qs.set('date', params.date)
if (params?.status) qs.set('status', params.status)
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}`),
},
// ── 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 }) =>
req<User>('POST', `/api/hotels/${slug}/users`, data),
update: (slug: string, id: string, data: Partial<{ name: string; email: string; password: string }>) =>
req<User>('PATCH', `/api/hotels/${slug}/users/${id}`, data),
delete: (slug: string, id: string) =>
req<void>('DELETE', `/api/hotels/${slug}/users/${id}`),
},
// ── Hotels ────────────────────────────────────────────────────────────────
hotels: {
get: (slug: string) =>
req<Hotel>('GET', `/api/hotels/${slug}`),
update: (slug: string, data: HotelPayload) =>
req<Hotel>('PATCH', `/api/hotels/${slug}`, toHotelPayload(data)),
},
// ── 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' = '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 }),
},
// ── 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 }>(
'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<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 }),
},
}
// ── 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
}
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
return out
}
export interface HkPayload {
room_id?: string; type?: string; priority?: string
status?: string; assignee_id?: string; notes?: string; due_date?: string
}
export interface HotelPayload {
name?: string; address?: string; phone?: string
timezone?: string; currency?: string
checkInTime?: string; checkOutTime?: string
}
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
created_at: string
}
export interface CategoryPayload {
name: string
description?: string
color?: string
amenities?: string[]
photos?: string[]
sort_order?: 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
}
export interface RateOverridePayload {
category_id: string
date: string
price: number
extra_person?: number
min_nights?: number
channel_prices?: Record<string, number>
closed?: boolean
}
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
}
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
}