feat: full Room Service backend integration

- Migrations 086-088: room_service_settings, menu_items, orders+order_items
- Backend: /api/hotels/:slug/room-service/* routes (settings, menu CRUD,
  orders CRUD, public config, public order status)
- push.ts: sendPushToHotel() — push to all hotel staff on new order
- On new order: in-app notification + push to hotel staff
- api.ts: RoomService types + roomService API methods
- RoomServicePage: replaced mocked state with real API, 30s polling,
  add/edit/delete menu items modal, real advance/cancel order
- GuestRoomServicePage: fetches real menu + config, uses hotel payment
  methods, submits real order, polls status every 15s

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-21 11:44:28 +03:00
parent 0e2180e3bc
commit 1975663cdd
9 changed files with 1169 additions and 418 deletions

View File

@@ -1,58 +1,18 @@
import { useState } from 'react'
import { useState, useEffect } from 'react'
import { useParams } from 'react-router-dom'
import {
ShoppingCart, Plus, Minus, X, Clock, CheckCircle2,
ChefHat, Truck, CreditCard, Receipt, ArrowLeft,
ChefHat, Truck, Receipt, ArrowLeft, AlertCircle, Loader2,
} from 'lucide-react'
import { cn, formatCurrency } from '../lib/utils'
import { api } from '../lib/api'
import type { RoomServiceMenuItem, RoomServicePublicConfig } from '../lib/api'
// ── Types ─────────────────────────────────────────────────────────────────────
interface MenuItem {
id: string
name: string
category: string
price: number
emoji: string
time: number
popular?: boolean
stopList?: boolean
}
const BASE = (import.meta.env.VITE_API_URL as string | undefined) ?? 'https://api.hotelsync.ru'
type Step = 'menu' | 'cart' | 'payment' | 'status'
type OrderStatus = 'new' | 'preparing' | 'ready' | 'delivered'
// ── Mock data (same menu as staff side) ───────────────────────────────────────
const MENU: MenuItem[] = [
{ id: 'm1', name: 'Яичница с беконом', category: 'Завтраки', price: 450, emoji: '🍳', time: 15, popular: true },
{ id: 'm2', name: 'Омлет с овощами', category: 'Завтраки', price: 380, emoji: '🥚', time: 12 },
{ id: 'm3', name: 'Сырники', category: 'Завтраки', price: 320, emoji: '🧀', time: 10 },
{ id: 'm4', name: 'Овсяная каша', category: 'Завтраки', price: 250, emoji: '🥣', time: 8 },
{ id: 'm5', name: 'Стейк рибай', category: 'Основные', price: 1800, emoji: '🥩', time: 25, popular: true },
{ id: 'm6', name: 'Куриная грудка', category: 'Основные', price: 750, emoji: '🍗', time: 20 },
{ id: 'm7', name: 'Паста карбонара', category: 'Основные', price: 650, emoji: '🍝', time: 18 },
{ id: 'm8', name: 'Лосось на гриле', category: 'Основные', price: 1200, emoji: '🐟', time: 22 },
{ id: 'm9', name: 'Цезарь с курицей', category: 'Закуски', price: 550, emoji: '🥗', time: 10, popular: true },
{ id: 'm10', name: 'Сэндвич клуб', category: 'Закуски', price: 420, emoji: '🥪', time: 8 },
{ id: 'm11', name: 'Сырная тарелка', category: 'Закуски', price: 680, emoji: '🧀', time: 5 },
{ id: 'm12', name: 'Кофе американо', category: 'Напитки', price: 180, emoji: '☕', time: 5, popular: true },
{ id: 'm13', name: 'Капучино', category: 'Напитки', price: 220, emoji: '☕', time: 6 },
{ id: 'm14', name: 'Свежевыжатый сок', category: 'Напитки', price: 280, emoji: '🍊', time: 5 },
{ id: 'm15', name: 'Вино красное бокал', category: 'Напитки', price: 450, emoji: '🍷', time: 3 },
{ id: 'm16', name: 'Тирамису', category: 'Десерты', price: 380, emoji: '🍮', time: 3 },
{ id: 'm17', name: 'Чизкейк', category: 'Десерты', price: 350, emoji: '🍰', time: 3 },
]
const CATEGORIES = [...new Set(MENU.map(i => i.category))]
const HOTEL_CONFIG: Record<string, { name: string; color: string; serviceChargePct: number }> = {
'grand-palace': { name: 'Гранд Палас', color: '#2563eb', serviceChargePct: 15 },
}
const DEFAULT_CONFIG = { name: 'Отель', color: '#2563eb', serviceChargePct: 15 }
// ── Status display ─────────────────────────────────────────────────────────────
const STATUS_STEPS: { key: OrderStatus; label: string; icon: React.ElementType; desc: string }[] = [
{ key: 'new', label: 'Принят', icon: Receipt, desc: 'Заказ отправлен на кухню' },
{ key: 'preparing', label: 'Готовится', icon: ChefHat, desc: 'Повар готовит ваш заказ' },
@@ -60,46 +20,124 @@ const STATUS_STEPS: { key: OrderStatus; label: string; icon: React.ElementType;
{ key: 'delivered', label: 'Доставлен', icon: Truck, desc: 'Курьер в пути к вашему номеру' },
]
// ── Component ─────────────────────────────────────────────────────────────────
export function GuestRoomServicePage() {
const { slug } = useParams<{ slug: string }>()
const config = (slug ? HOTEL_CONFIG[slug] : null) ?? DEFAULT_CONFIG
const [category, setCategory] = useState(CATEGORIES[0])
const [cart, setCart] = useState<Record<string, number>>({})
const [step, setStep] = useState<Step>('menu')
const [config, setConfig] = useState<RoomServicePublicConfig | null>(null)
const [menu, setMenu] = useState<RoomServiceMenuItem[]>([])
const [loadErr, setLoadErr] = useState('')
const [category, setCategory] = useState('')
const [cart, setCart] = useState<Record<string, number>>({})
const [step, setStep] = useState<Step>('menu')
const [roomNumber, setRoomNumber] = useState('')
const [notes, setNotes] = useState('')
const [payMode, setPayMode] = useState<'online' | 'bill'>('online')
const [guestName, setGuestName] = useState('')
const [notes, setNotes] = useState('')
const [payMethod, setPayMethod] = useState('')
const [submitting, setSubmitting] = useState(false)
const [submitErr, setSubmitErr] = useState('')
const [orderId, setOrderId] = useState('')
const [orderStatus, setOrderStatus] = useState<OrderStatus>('new')
const cartItems = MENU.filter(i => (cart[i.id] ?? 0) > 0)
const subtotal = cartItems.reduce((s, i) => s + i.price * cart[i.id], 0)
const svcCharge = Math.round(subtotal * config.serviceChargePct / 100)
const total = subtotal + svcCharge
const cartCount = Object.values(cart).reduce((s, n) => s + n, 0)
// ── Load config + menu ────────────────────────────────────────────────────
const add = (id: string) => setCart(p => ({ ...p, [id]: (p[id] ?? 0) + 1 }))
const sub = (id: string) => setCart(p => { const n = Math.max(0, (p[id] ?? 0) - 1); return { ...p, [id]: n } })
const canOrder = cartCount > 0 && roomNumber.trim().length > 0
useEffect(() => {
if (!slug) return
Promise.all([
api.roomService.getPublicConfig(slug),
api.roomService.getMenu(slug),
]).then(([cfg, items]) => {
setConfig(cfg)
setMenu(items)
if (items.length > 0) setCategory(items[0].category)
if (cfg.paymentMethods.length > 0) setPayMethod(cfg.paymentMethods[0].name)
}).catch(() => setLoadErr('Не удалось загрузить меню. Попробуйте позже.'))
}, [slug])
const placeOrder = () => {
// Simulate status progression
setStep('status')
setOrderStatus('new')
setTimeout(() => setOrderStatus('preparing'), 3000)
setTimeout(() => setOrderStatus('ready'), 8000)
setTimeout(() => setOrderStatus('delivered'), 13000)
// ── Poll order status ─────────────────────────────────────────────────────
useEffect(() => {
if (!orderId || !slug || orderStatus === 'delivered') return
const poll = async () => {
try {
const res = await fetch(`${BASE}/api/hotels/${slug}/room-service/orders/${orderId}/status`)
if (res.ok) {
const data = await res.json() as { status: string }
if (['new','preparing','ready','delivered'].includes(data.status)) {
setOrderStatus(data.status as OrderStatus)
}
}
} catch { /* ignore */ }
}
const id = setInterval(poll, 15000)
return () => clearInterval(id)
}, [orderId, slug, orderStatus])
// ── Cart logic ────────────────────────────────────────────────────────────
const add = (id: string) => setCart(p => ({ ...p, [id]: (p[id] ?? 0) + 1 }))
const sub = (id: string) => setCart(p => ({ ...p, [id]: Math.max(0, (p[id] ?? 0) - 1) }))
const cartItems = menu.filter(i => (cart[i.id] ?? 0) > 0)
const subtotal = cartItems.reduce((s, i) => s + Number(i.price) * cart[i.id], 0)
const svcCharge = Math.round(subtotal * (config?.serviceChargePct ?? 0) / 100)
const total = subtotal + svcCharge
const cartCount = Object.values(cart).reduce((s, n) => s + n, 0)
const canOrder = cartCount > 0 && roomNumber.trim().length > 0
// ── Submit order ──────────────────────────────────────────────────────────
const placeOrder = async () => {
if (!slug || !canOrder) return
setSubmitting(true)
setSubmitErr('')
try {
const order = await api.roomService.createOrder(slug, {
roomNumber: roomNumber.trim(),
guestName: guestName.trim() || undefined,
orderType: 'room',
paymentMethod: payMethod || undefined,
notes: notes.trim() || undefined,
items: cartItems.map(i => ({ menuItemId: i.id, quantity: cart[i.id] })),
})
setOrderId(order.id)
setOrderStatus('new')
setStep('status')
} catch {
setSubmitErr('Не удалось отправить заказ. Попробуйте ещё раз.')
} finally {
setSubmitting(false)
}
}
const color = config ? '#2563eb' : '#2563eb' // hotel color if available in future
const currentStatusIdx = STATUS_STEPS.findIndex(s => s.key === orderStatus)
const categories = [...new Set(menu.map(i => i.category))]
// ── Loading ───────────────────────────────────────────────────────────────
if (loadErr) return (
<div className="min-h-screen flex items-center justify-center bg-slate-50 p-4">
<div className="text-center">
<AlertCircle size={40} className="mx-auto mb-3 text-red-400" />
<p className="font-semibold text-slate-800">{loadErr}</p>
</div>
</div>
)
if (!config) return (
<div className="min-h-screen flex items-center justify-center bg-slate-50">
<Loader2 size={32} className="animate-spin text-slate-400" />
</div>
)
return (
<div className="min-h-screen bg-slate-50" style={{ fontFamily: 'system-ui, sans-serif' }}>
{/* Header */}
<div className="sticky top-0 z-20 shadow-sm" style={{ backgroundColor: config.color }}>
<div className="sticky top-0 z-20 shadow-sm" style={{ backgroundColor: color }}>
<div className="max-w-lg mx-auto px-4 py-3 flex items-center gap-3">
{step !== 'menu' && step !== 'status' && (
<button onClick={() => setStep(step === 'payment' ? 'cart' : 'menu')} className="text-white/80 hover:text-white">
@@ -107,7 +145,7 @@ export function GuestRoomServicePage() {
</button>
)}
<div className="flex-1">
<p className="text-white font-bold text-base leading-tight">{config.name}</p>
<p className="text-white font-bold text-base leading-tight">{config.hotelName}</p>
<p className="text-white/70 text-xs">Room Service</p>
</div>
{step === 'menu' && cartCount > 0 && (
@@ -128,9 +166,8 @@ export function GuestRoomServicePage() {
{/* ── MENU ── */}
{step === 'menu' && (
<div className="space-y-4">
{/* Category tabs */}
<div className="flex gap-2 overflow-x-auto pb-1 -mx-4 px-4">
{CATEGORIES.map(cat => (
{categories.map(cat => (
<button
key={cat}
onClick={() => setCategory(cat)}
@@ -140,36 +177,34 @@ export function GuestRoomServicePage() {
? 'text-white border-transparent'
: 'bg-white text-slate-700 border-slate-200',
)}
style={category === cat ? { backgroundColor: config.color, borderColor: config.color } : {}}
style={category === cat ? { backgroundColor: color, borderColor: color } : {}}
>
{cat}
</button>
))}
</div>
{/* Items */}
<div className="space-y-2">
{MENU.filter(i => i.category === category && !i.stopList).map(item => (
{menu.filter(i => i.category === category && !i.isStopList).map(item => (
<div key={item.id} className="bg-white rounded-2xl p-4 shadow-sm flex items-center gap-4">
<span className="text-3xl shrink-0">{item.emoji}</span>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5 flex-wrap">
<p className="font-semibold text-slate-900 text-sm">{item.name}</p>
{item.popular && (
{item.isPopular && (
<span className="text-[10px] bg-amber-100 text-amber-700 px-1.5 py-0.5 rounded-full font-medium">Хит</span>
)}
</div>
<p className="text-xs text-slate-400 flex items-center gap-1 mt-0.5">
<Clock size={10} />{item.time} мин
<Clock size={10} />{item.prepTimeMin} мин
</p>
<p className="font-bold text-slate-900 mt-1">{formatCurrency(item.price)}</p>
<p className="font-bold text-slate-900 mt-1">{formatCurrency(Number(item.price))}</p>
</div>
{/* Counter */}
{(cart[item.id] ?? 0) === 0 ? (
<button
onClick={() => add(item.id)}
className="w-9 h-9 rounded-full flex items-center justify-center text-white shadow-md transition-transform active:scale-90"
style={{ backgroundColor: config.color }}
style={{ backgroundColor: color }}
>
<Plus size={18} />
</button>
@@ -179,11 +214,7 @@ export function GuestRoomServicePage() {
<Minus size={14} />
</button>
<span className="text-sm font-bold w-4 text-center">{cart[item.id]}</span>
<button
onClick={() => add(item.id)}
className="w-8 h-8 rounded-full flex items-center justify-center text-white"
style={{ backgroundColor: config.color }}
>
<button onClick={() => add(item.id)} className="w-8 h-8 rounded-full flex items-center justify-center text-white" style={{ backgroundColor: color }}>
<Plus size={14} />
</button>
</div>
@@ -192,13 +223,12 @@ export function GuestRoomServicePage() {
))}
</div>
{/* Sticky cart button */}
{cartCount > 0 && (
<div className="sticky bottom-4">
<button
onClick={() => setStep('cart')}
className="w-full py-4 rounded-2xl text-white font-bold text-base shadow-xl transition-opacity hover:opacity-90 active:scale-95"
style={{ backgroundColor: config.color }}
style={{ backgroundColor: color }}
>
Перейти к заказу · {formatCurrency(total)}
</button>
@@ -218,42 +248,37 @@ export function GuestRoomServicePage() {
<span className="text-2xl">{item.emoji}</span>
<div className="flex-1">
<p className="text-sm font-medium text-slate-800">{item.name}</p>
<p className="text-xs text-slate-500">{formatCurrency(item.price)} × {cart[item.id]}</p>
<p className="text-xs text-slate-500">{formatCurrency(Number(item.price))} × {cart[item.id]}</p>
</div>
<div className="flex items-center gap-2">
<button onClick={() => sub(item.id)} className="w-7 h-7 rounded-full bg-slate-100 flex items-center justify-center text-slate-600">
{cart[item.id] === 1 ? <X size={12} /> : <Minus size={12} />}
</button>
<span className="text-sm font-bold w-4 text-center">{cart[item.id]}</span>
<button
onClick={() => add(item.id)}
className="w-7 h-7 rounded-full flex items-center justify-center text-white"
style={{ backgroundColor: config.color }}
>
<button onClick={() => add(item.id)} className="w-7 h-7 rounded-full flex items-center justify-center text-white" style={{ backgroundColor: color }}>
<Plus size={12} />
</button>
</div>
<span className="text-sm font-semibold text-slate-800 w-16 text-right">{formatCurrency(item.price * cart[item.id])}</span>
<span className="text-sm font-semibold text-slate-800 w-16 text-right">{formatCurrency(Number(item.price) * cart[item.id])}</span>
</div>
))}
<div className="px-4 py-3 bg-slate-50 space-y-1.5">
<div className="flex justify-between text-sm text-slate-600">
<span>Сумма заказа</span>
<span>{formatCurrency(subtotal)}</span>
</div>
<div className="flex justify-between text-sm text-slate-600">
<span>Обслуживание {config.serviceChargePct}%</span>
<span>+{formatCurrency(svcCharge)}</span>
<span>Сумма заказа</span><span>{formatCurrency(subtotal)}</span>
</div>
{svcCharge > 0 && (
<div className="flex justify-between text-sm text-slate-600">
<span>Обслуживание {config.serviceChargePct}%</span>
<span>+{formatCurrency(svcCharge)}</span>
</div>
)}
<div className="flex justify-between font-bold text-slate-900 text-base pt-1 border-t border-slate-200">
<span>Итого</span>
<span>{formatCurrency(total)}</span>
<span>Итого</span><span>{formatCurrency(total)}</span>
</div>
</div>
</div>
{/* Room number */}
<div className="bg-white rounded-2xl shadow-sm p-4 space-y-3">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1.5">Номер комнаты *</label>
@@ -265,6 +290,16 @@ export function GuestRoomServicePage() {
onChange={e => setRoomNumber(e.target.value)}
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1.5">Ваше имя (необязательно)</label>
<input
type="text"
className="w-full border border-slate-200 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:border-blue-400"
placeholder="Иван"
value={guestName}
onChange={e => setGuestName(e.target.value)}
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1.5">Пожелания (необязательно)</label>
<textarea
@@ -278,12 +313,12 @@ export function GuestRoomServicePage() {
</div>
<button
onClick={() => setStep('payment')}
onClick={() => config.paymentMethods.length > 0 ? setStep('payment') : placeOrder()}
disabled={!canOrder}
className="w-full py-4 rounded-2xl text-white font-bold text-base disabled:opacity-40 transition-opacity hover:opacity-90"
style={{ backgroundColor: config.color }}
style={{ backgroundColor: color }}
>
К оплате
{config.paymentMethods.length > 0 ? 'К оплате' : 'Оформить заказ'}
</button>
</div>
)}
@@ -293,61 +328,61 @@ export function GuestRoomServicePage() {
<div className="space-y-4">
<h2 className="font-bold text-slate-900 text-lg">Способ оплаты</h2>
{[
{ key: 'online' as const, icon: CreditCard, label: 'Оплатить онлайн сейчас', desc: 'Банковской картой через защищённую форму' },
{ key: 'bill' as const, icon: Receipt, label: 'Включить в счёт при выезде', desc: 'Оплата при расчёте в конце проживания' },
].map(opt => (
{config.paymentMethods.map(method => (
<button
key={opt.key}
onClick={() => setPayMode(opt.key)}
key={method.id}
onClick={() => setPayMethod(method.name)}
className={cn(
'w-full flex items-center gap-4 p-4 rounded-2xl border-2 text-left bg-white transition-colors',
payMode === opt.key ? 'border-blue-500' : 'border-slate-200',
payMethod === method.name ? 'border-blue-500' : 'border-slate-200',
)}
style={payMode === opt.key ? { borderColor: config.color } : {}}
style={payMethod === method.name ? { borderColor: color } : {}}
>
<div
className="w-10 h-10 rounded-xl flex items-center justify-center shrink-0"
style={payMode === opt.key ? { backgroundColor: `${config.color}20` } : { backgroundColor: '#f1f5f9' }}
className="w-10 h-10 rounded-xl flex items-center justify-center shrink-0 text-lg"
style={payMethod === method.name ? { backgroundColor: `${color}20` } : { backgroundColor: '#f1f5f9' }}
>
<opt.icon size={18} style={payMode === opt.key ? { color: config.color } : { color: '#64748b' }} />
{method.type === 'cash' ? '💵' : method.type === 'card' ? '💳' : method.type === 'transfer' ? '📲' : '🏦'}
</div>
<div>
<p className="font-semibold text-slate-900 text-sm">{opt.label}</p>
<p className="text-xs text-slate-500 mt-0.5">{opt.desc}</p>
<div className="flex-1">
<p className="font-semibold text-slate-900 text-sm">{method.name}</p>
</div>
<div className={cn(
'ml-auto w-5 h-5 rounded-full border-2 flex items-center justify-center shrink-0',
payMode === opt.key ? 'border-blue-500' : 'border-slate-300',
)} style={payMode === opt.key ? { borderColor: config.color } : {}}>
{payMode === opt.key && (
<div className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: config.color }} />
)}
<div
className={cn('w-5 h-5 rounded-full border-2 flex items-center justify-center shrink-0', payMethod === method.name ? 'border-blue-500' : 'border-slate-300')}
style={payMethod === method.name ? { borderColor: color } : {}}
>
{payMethod === method.name && <div className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: color }} />}
</div>
</button>
))}
{/* Summary */}
<div className="bg-white rounded-2xl shadow-sm p-4">
<p className="text-sm font-semibold text-slate-700 mb-2">Номер {roomNumber}</p>
{cartItems.map(i => (
<div key={i.id} className="flex justify-between text-sm text-slate-600 py-0.5">
<span>{i.emoji} {i.name} ×{cart[i.id]}</span>
<span>{formatCurrency(i.price * cart[i.id])}</span>
<span>{formatCurrency(Number(i.price) * cart[i.id])}</span>
</div>
))}
<div className="flex justify-between font-bold text-slate-900 text-base mt-2 pt-2 border-t border-slate-100">
<span>Итого</span>
<span>{formatCurrency(total)}</span>
<span>Итого</span><span>{formatCurrency(total)}</span>
</div>
</div>
{submitErr && (
<div className="flex items-center gap-2 p-3 rounded-xl bg-red-50 border border-red-200 text-sm text-red-700">
<AlertCircle size={14} />{submitErr}
</div>
)}
<button
onClick={placeOrder}
className="w-full py-4 rounded-2xl text-white font-bold text-base transition-opacity hover:opacity-90"
style={{ backgroundColor: config.color }}
disabled={!payMethod || submitting}
className="w-full py-4 rounded-2xl text-white font-bold text-base transition-opacity hover:opacity-90 disabled:opacity-40 flex items-center justify-center gap-2"
style={{ backgroundColor: color }}
>
{payMode === 'online' ? `Оплатить ${formatCurrency(total)}` : 'Оформить заказ'}
{submitting && <Loader2 size={16} className="animate-spin" />}
Оформить заказ {formatCurrency(total)}
</button>
</div>
)}
@@ -358,15 +393,15 @@ export function GuestRoomServicePage() {
<div className="text-center">
<div
className="w-20 h-20 rounded-full mx-auto flex items-center justify-center text-4xl mb-4"
style={{ backgroundColor: `${config.color}15` }}
style={{ backgroundColor: `${color}15` }}
>
🎉
</div>
<h2 className="text-xl font-bold text-slate-900">Заказ принят!</h2>
<p className="text-slate-500 text-sm mt-1">Номер {roomNumber} · {formatCurrency(total)}</p>
{payMethod && <p className="text-slate-400 text-xs mt-0.5">Оплата: {payMethod}</p>}
</div>
{/* Progress */}
<div className="bg-white rounded-2xl shadow-sm p-5">
<p className="text-sm font-semibold text-slate-700 mb-4">Статус заказа</p>
<div className="space-y-4">
@@ -376,25 +411,23 @@ export function GuestRoomServicePage() {
const Icon = s.icon
return (
<div key={s.key} className="flex items-center gap-3">
<div className={cn(
'w-9 h-9 rounded-full flex items-center justify-center shrink-0 transition-colors',
done ? 'text-white' : 'bg-slate-100 text-slate-400',
)} style={done ? { backgroundColor: config.color } : {}}>
<div
className={cn('w-9 h-9 rounded-full flex items-center justify-center shrink-0 transition-colors', done ? 'text-white' : 'bg-slate-100 text-slate-400')}
style={done ? { backgroundColor: color } : {}}
>
<Icon size={16} />
</div>
<div className="flex-1">
<p className={cn('text-sm font-semibold', done ? 'text-slate-900' : 'text-slate-400')}>
{s.label}
</p>
<p className={cn('text-sm font-semibold', done ? 'text-slate-900' : 'text-slate-400')}>{s.label}</p>
{current && <p className="text-xs text-slate-500 mt-0.5">{s.desc}</p>}
</div>
{current && (
{current && orderStatus !== 'delivered' && (
<div className="flex gap-0.5">
{[0,1,2].map(i => (
<div
key={i}
className="w-1.5 h-1.5 rounded-full animate-bounce"
style={{ backgroundColor: config.color, animationDelay: `${i * 0.15}s` }}
style={{ backgroundColor: color, animationDelay: `${i * 0.15}s` }}
/>
))}
</div>
@@ -403,12 +436,15 @@ export function GuestRoomServicePage() {
)
})}
</div>
{orderStatus !== 'delivered' && (
<p className="text-xs text-slate-400 mt-4 text-center">Статус обновляется автоматически</p>
)}
</div>
<button
onClick={() => { setCart({}); setStep('menu'); setRoomNumber(''); setNotes('') }}
onClick={() => { setCart({}); setStep('menu'); setRoomNumber(''); setGuestName(''); setNotes(''); setOrderId('') }}
className="w-full py-3 rounded-2xl border-2 text-sm font-semibold transition-colors"
style={{ borderColor: config.color, color: config.color }}
style={{ borderColor: color, color: color }}
>
Сделать ещё заказ
</button>