From bbea04ce2e945146e07dc69cc80e919e142a3031 Mon Sep 17 00:00:00 2001 From: HotelSync Date: Fri, 10 Apr 2026 11:24:11 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20widget=20=E2=80=94=20editable=20additio?= =?UTF-8?q?nal=20services=20with=20add/edit/delete=20+=20DB=20persistence?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Services can now be added, edited inline (icon/name/price/unit), and deleted - List persisted to DB as widget_services key in hotel_settings (auto-save) - Services loaded from DB on settings page init; fall back to DEFAULT_SERVICES - Toggle enable/disable retained; edit/delete buttons appear on row hover Co-Authored-By: Claude Sonnet 4.6 --- src/pages/BookingWidgetPage.tsx | 160 +++++++++++++++++++++++++++++--- 1 file changed, 145 insertions(+), 15 deletions(-) diff --git a/src/pages/BookingWidgetPage.tsx b/src/pages/BookingWidgetPage.tsx index 38a5f69..f03142c 100644 --- a/src/pages/BookingWidgetPage.tsx +++ b/src/pages/BookingWidgetPage.tsx @@ -1248,6 +1248,7 @@ 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, })) setSettingsReady(true) }).finally(() => setGatewayLoading(false)) @@ -1276,6 +1277,9 @@ export function BookingWidgetPage() { const [pageTab, setPageTab] = useState<'constructor' | 'code' | 'stats'>('constructor') const [newFieldLabel, setNewFieldLabel] = useState('') const [showAddField, setShowAddField] = useState(false) + const [showAddService, setShowAddService] = useState(false) + const [editingServiceId, setEditingServiceId] = useState(null) + const [newService, setNewService] = useState>({ name: '', icon: '✨', price: 0, unit: 'раз' }) const set = (k: K, v: WidgetSettings[K]) => setSettings(prev => ({ ...prev, [k]: v })) @@ -1296,6 +1300,7 @@ export function BookingWidgetPage() { widget_show_promo: s.showPromo, widget_extra_beds: s.allowExtraBeds, widget_children: s.allowChildren, + widget_services: s.additionalServices, }) setSaved(true) setTimeout(() => setSaved(false), 2000) @@ -1320,6 +1325,34 @@ export function BookingWidgetPage() { ), })) + const addService = () => { + if (!newService.name.trim()) return + setSettings(prev => ({ + ...prev, + additionalServices: [...prev.additionalServices, { + id: `svc-${Date.now()}`, + ...newService, + enabled: true, + }], + })) + setNewService({ name: '', icon: '✨', price: 0, unit: 'раз' }) + setShowAddService(false) + } + + const updateService = (id: string, patch: Partial) => + setSettings(prev => ({ + ...prev, + additionalServices: prev.additionalServices.map(s => + s.id === id ? { ...s, ...patch } : s + ), + })) + + const removeService = (id: string) => + setSettings(prev => ({ + ...prev, + additionalServices: prev.additionalServices.filter(s => s.id !== id), + })) + const toggleField = (id: string, prop: 'enabled' | 'required') => setSettings(prev => ({ ...prev, @@ -1562,25 +1595,122 @@ export function BookingWidgetPage() { {/* Additional services */}
-

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

-

- Услуги, доступные для добавления при бронировании -

-
+
+
+

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

+

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

+
+ +
+ +
{settings.additionalServices.map(s => ( -
- {s.icon} - {s.name} - {s.price.toLocaleString('ru-RU')} ₽/{s.unit} - +
+ {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="ч/день" + /> + + +
+ )}
{/* Form fields */}