Refactor POS/Reviews/RoomService/BookingWidget/BookingModal
- POS: complete rewrite — shift management + room payments (not products); ATOL placeholder; room balance sidebar; receipts log - BookingModal: remove "Источник" field; restructure layout (notes+summary as bottom full-width row, eliminating scroll) - Reviews: negative (<2 stars = <4/10) → moderation with private manager reply; positive → redirect to external platforms (Booking.com, Яндекс, Google); new tabs: "Требуют ответа" / "Перенаправить" - RoomService: add orderType (room delivery / restaurant table); service charge % for room delivery; scheduledTime for restaurant; paymentStatus (online_paid / pending); settings tab - BookingWidget: complete redesign — live widget constructor with color picker, language, rooms+rental tabs, payment provider selector; live preview; install code tab Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,8 +2,8 @@ import { useState } from 'react'
|
||||
import { format } from 'date-fns'
|
||||
import { Star, Banknote, CreditCard, AlertCircle, Map } from 'lucide-react'
|
||||
import { Modal } from '../ui/Modal'
|
||||
import { cn, BOOKING_STATUS_LABELS, SOURCE_LABELS } from '../../lib/utils'
|
||||
import type { Booking, DraftBooking, Room, BookingStatus, BookingSource } from '../../types'
|
||||
import { cn, BOOKING_STATUS_LABELS } from '../../lib/utils'
|
||||
import type { Booking, DraftBooking, Room, BookingStatus } from '../../types'
|
||||
import { FloorMapModal } from '../floormap/FloorMapModal'
|
||||
|
||||
const GUEST_TAGS = ['VIP', 'Постоянный гость', 'Корпоративный', 'Медовый месяц', 'День рождения', 'Особые пожелания']
|
||||
@@ -21,16 +21,15 @@ interface BookingModalProps {
|
||||
|
||||
export function BookingModal({ open, draft, rooms, onClose, onSave, existing }: BookingModalProps) {
|
||||
const [form, setForm] = useState({
|
||||
roomId: existing?.roomId ?? draft.roomId,
|
||||
guestName: existing?.guestName ?? '',
|
||||
guestEmail: existing?.guestEmail ?? '',
|
||||
checkIn: existing?.checkIn ?? draft.checkIn,
|
||||
checkOut: existing?.checkOut ?? draft.checkOut,
|
||||
adults: existing?.adults ?? 2,
|
||||
children: existing?.children ?? 0,
|
||||
source: (existing?.source ?? 'direct') as BookingSource,
|
||||
status: (existing?.status ?? 'confirmed') as BookingStatus,
|
||||
notes: existing?.notes ?? '',
|
||||
roomId: existing?.roomId ?? draft.roomId,
|
||||
guestName: existing?.guestName ?? '',
|
||||
guestEmail:existing?.guestEmail ?? '',
|
||||
checkIn: existing?.checkIn ?? draft.checkIn,
|
||||
checkOut: existing?.checkOut ?? draft.checkOut,
|
||||
adults: existing?.adults ?? 2,
|
||||
children: existing?.children ?? 0,
|
||||
status: (existing?.status ?? 'confirmed') as BookingStatus,
|
||||
notes: existing?.notes ?? '',
|
||||
})
|
||||
const [guestTags, setGuestTags] = useState<string[]>([])
|
||||
const [paymentMethod, setPaymentMethod] = useState<'cash' | 'terminal' | null>(null)
|
||||
@@ -79,6 +78,7 @@ export function BookingModal({ open, draft, rooms, onClose, onSave, existing }:
|
||||
|
||||
onSave({
|
||||
...form,
|
||||
source: 'direct',
|
||||
checkIn,
|
||||
checkOut,
|
||||
notes: hourlyPrefix + form.notes,
|
||||
@@ -109,9 +109,10 @@ export function BookingModal({ open, draft, rooms, onClose, onSave, existing }:
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-6">
|
||||
{/* ── LEFT COLUMN ── */}
|
||||
<div className="flex-1 space-y-4 min-w-0">
|
||||
<div className="flex-1 space-y-3 min-w-0">
|
||||
{/* Room */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
@@ -271,9 +272,9 @@ export function BookingModal({ open, draft, rooms, onClose, onSave, existing }:
|
||||
)}
|
||||
|
||||
{/* Adults + Children + Status */}
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||
Взрослых
|
||||
</label>
|
||||
<input
|
||||
@@ -312,7 +313,7 @@ export function BookingModal({ open, draft, rooms, onClose, onSave, existing }:
|
||||
</div>
|
||||
|
||||
{/* ── RIGHT COLUMN ── */}
|
||||
<div className="flex-1 space-y-4 min-w-0">
|
||||
<div className="flex-1 space-y-3 min-w-0">
|
||||
{/* Guest tags */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
|
||||
@@ -344,30 +345,6 @@ export function BookingModal({ open, draft, rooms, onClose, onSave, existing }:
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Source */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
|
||||
Источник
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(Object.entries(SOURCE_LABELS) as [BookingSource, string][]).map(([k, v]) => (
|
||||
<button
|
||||
key={k}
|
||||
type="button"
|
||||
onClick={() => set('source', k)}
|
||||
className={cn(
|
||||
'px-3 py-1.5 rounded-lg text-sm font-medium border transition-colors',
|
||||
form.source === k
|
||||
? 'bg-brand-600 text-white border-brand-600'
|
||||
: 'bg-white dark:bg-slate-700 text-slate-700 dark:text-slate-300 border-slate-200 dark:border-slate-600 hover:border-brand-400',
|
||||
)}
|
||||
>
|
||||
{v}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payment */}
|
||||
{total > 0 && (
|
||||
<div className="rounded-xl border border-slate-200 dark:border-slate-600 overflow-hidden">
|
||||
@@ -423,61 +400,63 @@ export function BookingModal({ open, draft, rooms, onClose, onSave, existing }:
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Notes */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
|
||||
Примечания
|
||||
</label>
|
||||
<textarea
|
||||
className="input resize-none"
|
||||
rows={2}
|
||||
placeholder="Дополнительные пожелания..."
|
||||
value={form.notes}
|
||||
onChange={e => set('notes', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
{total > 0 && room && (
|
||||
<div className="rounded-xl bg-slate-50 dark:bg-slate-700/50 px-4 py-3 space-y-1.5">
|
||||
{isHourly && room.allowHourly ? (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-300">
|
||||
{hourlyHours} {hourlyHours === 1 ? 'час' : hourlyHours < 5 ? 'часа' : 'часов'} × {(room.hourlyRate ?? 0).toLocaleString('ru-RU')} ₽
|
||||
</span>
|
||||
<span className="text-lg font-bold text-slate-900 dark:text-slate-100">
|
||||
{total.toLocaleString('ru-RU')} ₽
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-300">
|
||||
{nights} {nights === 1 ? 'ночь' : nights < 5 ? 'ночи' : 'ночей'} × {room.baseRate.toLocaleString('ru-RU')} ₽
|
||||
</span>
|
||||
<span className="text-lg font-bold text-slate-900 dark:text-slate-100">
|
||||
{total.toLocaleString('ru-RU')} ₽
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{paidAmount > 0 && (
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-emerald-600 dark:text-emerald-400">Оплачено</span>
|
||||
<span className="font-semibold text-emerald-600 dark:text-emerald-400">
|
||||
{paidAmount.toLocaleString('ru-RU')} ₽
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{debt > 0 && (
|
||||
<div className="flex items-center gap-1.5 text-sm text-red-600 dark:text-red-400">
|
||||
<AlertCircle size={13} />
|
||||
<span className="flex-1">{isFutureBooking ? 'Задолженность' : 'Долг при заселении'}</span>
|
||||
<span className="font-semibold">{debt.toLocaleString('ru-RU')} ₽</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── BOTTOM ROW: Notes + Summary ── */}
|
||||
<div className="flex gap-6">
|
||||
<div className="flex-1">
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
|
||||
Примечания
|
||||
</label>
|
||||
<textarea
|
||||
className="input resize-none"
|
||||
rows={2}
|
||||
placeholder="Дополнительные пожелания..."
|
||||
value={form.notes}
|
||||
onChange={e => set('notes', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{total > 0 && room && (
|
||||
<div className="flex-1 rounded-xl bg-slate-50 dark:bg-slate-700/50 px-4 py-3 space-y-1.5 self-end">
|
||||
{isHourly && room.allowHourly ? (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-300">
|
||||
{hourlyHours} {hourlyHours === 1 ? 'час' : hourlyHours < 5 ? 'часа' : 'часов'} × {(room.hourlyRate ?? 0).toLocaleString('ru-RU')} ₽
|
||||
</span>
|
||||
<span className="text-xl font-bold text-slate-900 dark:text-slate-100">
|
||||
{total.toLocaleString('ru-RU')} ₽
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-300">
|
||||
{nights} {nights === 1 ? 'ночь' : nights < 5 ? 'ночи' : 'ночей'} × {room.baseRate.toLocaleString('ru-RU')} ₽
|
||||
</span>
|
||||
<span className="text-xl font-bold text-slate-900 dark:text-slate-100">
|
||||
{total.toLocaleString('ru-RU')} ₽
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{paidAmount > 0 && (
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-emerald-600 dark:text-emerald-400">Оплачено</span>
|
||||
<span className="font-semibold text-emerald-600 dark:text-emerald-400">
|
||||
{paidAmount.toLocaleString('ru-RU')} ₽
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{debt > 0 && (
|
||||
<div className="flex items-center gap-1.5 text-sm text-red-600 dark:text-red-400">
|
||||
<AlertCircle size={13} />
|
||||
<span className="flex-1">{isFutureBooking ? 'Задолженность' : 'Долг при заселении'}</span>
|
||||
<span className="font-semibold">{debt.toLocaleString('ru-RU')} ₽</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{showFloorMap && (
|
||||
|
||||
Reference in New Issue
Block a user