Connect ChannelsPage, FloorMapPage, SettingsPage, UsersPage to real API

- ChannelsPage: load from API, toggle/sync call real endpoints; normalize
  enabled→isEnabled, lastSyncedAt→lastSyncAt, add displayName/mappings defaults
- FloorMapPage: load rooms+bookings from API; create booking via API
- SettingsPage: load hotel via GET /api/hotels/:slug; save general section
  via PATCH (name, address, timezone, currency, check_in/out times)
- UsersPage: load users from API; create/update/delete via API;
  map backend User (name/role) → StaffUser (firstName/lastName/StaffRole)
- api.ts: add hotels.get/update, users.delete, channel normalization,
  HotelPayload/toHotelPayload
- types/index.ts: extend Hotel with phone/checkInTime/checkOutTime/optional fields;
  add User.createdAt/updatedAt
- backend/routes/hotels.ts: extend PATCH to allow address/phone/check_in_time/check_out_time
- backend/migrations/004_hotels_contacts.sql: add address/phone/check_in/out_time to hotels

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-17 14:21:45 +03:00
parent 57162c55c6
commit 55a34dc0dc
9 changed files with 277 additions and 109 deletions

View File

@@ -1,4 +1,4 @@
import type { Room, Booking, HousekeepingTask, Channel, User } from '../types'
import type { Room, Booking, HousekeepingTask, Channel, ChannelName, Hotel, User } from '../types'
// ── Base URL ────────────────────────────────────────────────────────────────
@@ -104,6 +104,34 @@ async function req<T>(
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 = {
@@ -177,14 +205,20 @@ export const api = {
// ── Channels ──────────────────────────────────────────────────────────────
channels: {
list: (slug: string) =>
req<Channel[]>('GET', `/api/hotels/${slug}/channels`),
list: async (slug: string): Promise<Channel[]> => {
const raw = await req<Record<string, unknown>[]>('GET', `/api/hotels/${slug}/channels`)
return raw.map(normalizeChannel)
},
update: (slug: string, id: string, data: { enabled?: boolean; api_key?: string }) =>
req<Channel>('PATCH', `/api/hotels/${slug}/channels/${id}`, data),
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: (slug: string, id: string) =>
req<{ channel: Channel; synced_bookings: number }>('POST', `/api/hotels/${slug}/channels/${id}/sync`),
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 ─────────────────────────────────────────────────────────────────
@@ -197,6 +231,18 @@ export const api = {
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)),
},
}
@@ -264,3 +310,21 @@ 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
}
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
}