From c153e1254358f61d5c0ccd02c7fb4bcb5b7b8266 Mon Sep 17 00:00:00 2001 From: HotelSync Date: Fri, 10 Apr 2026 12:13:13 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20widget=20additional=20services=20?= =?UTF-8?q?=E2=80=94=20sourced=20from=20rental=20objects=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Additional services in widget are now derived from rental_objects (not a separate list) - Settings page shows rental objects with enable/disable toggle; link to rental module - Saves only {id, enabled}[] to widget_services — rental objects manage name/price/icon - Standalone widget also derives additionalServices from config.rentalObjects - Widget config API includes services[] in widgetSettings Co-Authored-By: Claude Sonnet 4.6 --- backend/src/routes/publicWidget.ts | 1 + src/lib/api.ts | 1 + src/pages/BookingWidgetPage.tsx | 161 ++++++---------------- src/pages/BookingWidgetStandalonePage.tsx | 36 +++-- 4 files changed, 73 insertions(+), 126 deletions(-) diff --git a/backend/src/routes/publicWidget.ts b/backend/src/routes/publicWidget.ts index b83003d..dce75cb 100644 --- a/backend/src/routes/publicWidget.ts +++ b/backend/src/routes/publicWidget.ts @@ -78,6 +78,7 @@ const publicWidget: FastifyPluginAsync = async (fastify) => { allowChildren: (ws.widget_children as boolean) ?? true, hotelName: (ws.widget_hotel_name as string) ?? hotel.name, bankDetails: (ws.widget_bank_details as string) ?? '', + services: Array.isArray(ws.widget_services) ? ws.widget_services as { id: string; enabled: boolean }[] : null, }, rooms: rooms.map(r => ({ id: r.id, diff --git a/src/lib/api.ts b/src/lib/api.ts index 99a9ccb..d8dc83f 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1716,6 +1716,7 @@ export interface WidgetConfig { allowExtraBeds: boolean allowChildren: boolean hotelName: string + services?: { id: string; enabled: boolean }[] } } diff --git a/src/pages/BookingWidgetPage.tsx b/src/pages/BookingWidgetPage.tsx index 4136307..db4c9a5 100644 --- a/src/pages/BookingWidgetPage.tsx +++ b/src/pages/BookingWidgetPage.tsx @@ -1313,7 +1313,20 @@ export function BookingWidgetPage() { showPromo: (hs as any).widgetShowPromo !== undefined ? Boolean((hs as any).widgetShowPromo) : prev.showPromo, allowExtraBeds: (hs as any).widgetExtraBeds !== undefined ? Boolean((hs as any).widgetExtraBeds) : prev.allowExtraBeds, allowChildren: (hs as any).widgetChildren !== undefined ? Boolean((hs as any).widgetChildren) : prev.allowChildren, - additionalServices: Array.isArray((hs as any).widgetServices) ? (hs as any).widgetServices : prev.additionalServices, + additionalServices: (() => { + const rentalObjs = config?.rentalObjects ?? [] + if (rentalObjs.length === 0) return prev.additionalServices + const saved: { id: string; enabled: boolean }[] = Array.isArray((hs as any).widgetServices) + ? (hs as any).widgetServices : [] + return rentalObjs.map(o => ({ + id: o.id, + name: o.name, + icon: o.icon, + price: o.pricePerHour, + unit: 'ч', + enabled: saved.length > 0 ? (saved.find(s => s.id === o.id)?.enabled ?? true) : true, + })) + })(), })) setSettingsReady(true) }).finally(() => setGatewayLoading(false)) @@ -1365,7 +1378,7 @@ export function BookingWidgetPage() { widget_show_promo: s.showPromo, widget_extra_beds: s.allowExtraBeds, widget_children: s.allowChildren, - widget_services: s.additionalServices, + widget_services: s.additionalServices.map(s => ({ id: s.id, enabled: s.enabled })), }) setSaved(true) setTimeout(() => setSaved(false), 2000) @@ -1658,122 +1671,38 @@ export function BookingWidgetPage() { - {/* Additional services */} + {/* Additional services — from rental objects */}
-
-
-

Дополнительные услуги

-

- Платные услуги, доступные гостю при бронировании номера -

+
+

Дополнительные услуги

+

+ Объекты аренды, доступные гостю при бронировании номера. Управляйте объектами в{' '} + модуле аренды. +

+
+ + {settings.additionalServices.length === 0 ? ( +
+

Нет объектов аренды

+ + Добавить в модуле аренды → +
- -
- -
- {settings.additionalServices.map(s => ( -
- {editingServiceId === s.id ? ( -
- updateService(s.id, { icon: e.target.value })} - className="w-10 text-center text-base border border-slate-200 rounded-lg p-1 bg-white dark:bg-slate-800 dark:border-slate-600" - placeholder="🎾" - maxLength={4} - /> - updateService(s.id, { name: e.target.value })} - className="flex-1 text-sm border border-slate-200 rounded-lg px-2 py-1 bg-white dark:bg-slate-800 dark:border-slate-600" - placeholder="Название" - /> - updateService(s.id, { price: Number(e.target.value) })} - className="w-20 text-sm border border-slate-200 rounded-lg px-2 py-1 bg-white dark:bg-slate-800 dark:border-slate-600" - placeholder="Цена" - min={0} - /> - updateService(s.id, { unit: e.target.value })} - className="w-16 text-sm border border-slate-200 rounded-lg px-2 py-1 bg-white dark:bg-slate-800 dark:border-slate-600" - placeholder="ч/день" - /> - -
- ) : ( -
- {s.icon} - {s.name} - {s.price.toLocaleString('ru-RU')} ₽/{s.unit} - - - -
- )} -
- ))} - - {settings.additionalServices.length === 0 && !showAddService && ( -

Нет дополнительных услуг

- )} -
- - {showAddService && ( -
- setNewService(prev => ({ ...prev, icon: e.target.value }))} - className="w-10 text-center text-base border border-slate-200 rounded-lg p-1 bg-white dark:bg-slate-800 dark:border-slate-600" - placeholder="🎾" - maxLength={4} - /> - setNewService(prev => ({ ...prev, name: e.target.value }))} - className="flex-1 text-sm border border-slate-200 rounded-lg px-2 py-1 bg-white dark:bg-slate-800 dark:border-slate-600" - placeholder="Название услуги" - autoFocus - /> - setNewService(prev => ({ ...prev, price: Number(e.target.value) }))} - className="w-20 text-sm border border-slate-200 rounded-lg px-2 py-1 bg-white dark:bg-slate-800 dark:border-slate-600" - placeholder="Цена" - min={0} - /> - setNewService(prev => ({ ...prev, unit: e.target.value }))} - className="w-16 text-sm border border-slate-200 rounded-lg px-2 py-1 bg-white dark:bg-slate-800 dark:border-slate-600" - placeholder="ч/день" - /> - - + ) : ( +
+ {settings.additionalServices.map(s => ( +
+ {s.icon} + {s.name} + {s.price.toLocaleString('ru-RU')} ₽/{s.unit} + +
+ ))}
)}
diff --git a/src/pages/BookingWidgetStandalonePage.tsx b/src/pages/BookingWidgetStandalonePage.tsx index 2835bf3..9520bdb 100644 --- a/src/pages/BookingWidgetStandalonePage.tsx +++ b/src/pages/BookingWidgetStandalonePage.tsx @@ -37,22 +37,38 @@ export function BookingWidgetStandalonePage() { setRealCategories(config.categories ?? []) setRealRentalObjects(config.rentalObjects ?? []) setPaymentEnabled(config.paymentEnabled) + const rentalObjs = config.rentalObjects ?? [] + const deriveServices = (savedServices: { id: string; enabled: boolean }[] | null | undefined) => + rentalObjs.map(o => ({ + id: o.id, + name: o.name, + icon: o.icon, + price: o.pricePerHour, + unit: 'ч', + enabled: savedServices ? (savedServices.find(s => s.id === o.id)?.enabled ?? true) : true, + })) + if (config.widgetSettings) { const ws = config.widgetSettings setSettings(prev => ({ ...prev, - hotelName: ws.hotelName ?? config.hotelName ?? prev.hotelName, - primaryColor: ws.primaryColor ?? prev.primaryColor, - language: (ws.language ?? prev.language) as 'ru' | 'en', - roomDisplayMode: (ws.roomDisplayMode ?? prev.roomDisplayMode) as 'rooms' | 'categories', - minNights: ws.minNights ?? prev.minNights, - showRental: ws.showRental ?? prev.showRental, - showPromo: ws.showPromo ?? prev.showPromo, - allowExtraBeds: ws.allowExtraBeds ?? prev.allowExtraBeds, - allowChildren: ws.allowChildren ?? prev.allowChildren, + hotelName: ws.hotelName ?? config.hotelName ?? prev.hotelName, + primaryColor: ws.primaryColor ?? prev.primaryColor, + language: (ws.language ?? prev.language) as 'ru' | 'en', + roomDisplayMode: (ws.roomDisplayMode ?? prev.roomDisplayMode) as 'rooms' | 'categories', + minNights: ws.minNights ?? prev.minNights, + showRental: ws.showRental ?? prev.showRental, + showPromo: ws.showPromo ?? prev.showPromo, + allowExtraBeds: ws.allowExtraBeds ?? prev.allowExtraBeds, + allowChildren: ws.allowChildren ?? prev.allowChildren, + additionalServices: rentalObjs.length > 0 ? deriveServices(ws.services) : prev.additionalServices, })) } else { - setSettings(prev => ({ ...prev, hotelName: config.hotelName ?? prev.hotelName })) + setSettings(prev => ({ + ...prev, + hotelName: config.hotelName ?? prev.hotelName, + additionalServices: rentalObjs.length > 0 ? deriveServices(null) : prev.additionalServices, + })) } }) .catch(() => {})