New module pages: POS (shift/receipt), Reviews (moderation+QR link), Room Service (live orders+menu), Rental (object CRUD+bookings)
- PosPage: shift timer, product catalog with categories, cart/receipt, cash/terminal payment, shift revenue summary - ReviewsPage: review moderation (approve/reject/reply), public review link + redirect config, NPS/avg rating stats - RoomServicePage: live orders with status pipeline (new→preparing→ready→delivered), menu tab with stop-list support - RentalPage: rental object management (create/edit/delete with icon+color picker), bookings table per object - BookingsPage: "Новая аренда" button in rental tab → 2-step flow (pick object+date → booking form) - App.tsx: routes for /pos, /reviews, /room-service, /rental Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import { RENTAL_OBJECTS, MOCK_RENTAL_BOOKINGS } from '../data/rentalData'
|
||||
import { useModules } from '../contexts/ModulesContext'
|
||||
import type { Booking, BookingStatus } from '../types'
|
||||
import type { RentalBooking } from '../data/rentalData'
|
||||
import { RentalBookingModal } from '../components/rental/RentalBookingModal'
|
||||
import { cn, BOOKING_STATUS_BADGE, BOOKING_STATUS_LABELS, SOURCE_COLORS, SOURCE_LABELS, formatCurrency, nightsCount } from '../lib/utils'
|
||||
import { Badge } from '../components/ui/Badge'
|
||||
import { BookingModal } from '../components/bookings/BookingModal'
|
||||
@@ -41,7 +42,11 @@ export function BookingsPage() {
|
||||
|
||||
const [tab, setTab] = useState<Tab>('rooms')
|
||||
const [bookings, setBookings] = useState<Booking[]>(MOCK_BOOKINGS)
|
||||
const [rentalBookings] = useState<RentalBooking[]>(MOCK_RENTAL_BOOKINGS)
|
||||
const [rentalBookings, setRentalBookings] = useState<RentalBooking[]>(MOCK_RENTAL_BOOKINGS)
|
||||
const [newRentalStep, setNewRentalStep] = useState<'idle' | 'pick'>('idle')
|
||||
const [rentalPickObj, setRentalPickObj] = useState(RENTAL_OBJECTS[0]?.id ?? '')
|
||||
const [rentalPickDate, setRentalPickDate] = useState(format(new Date(), 'yyyy-MM-dd'))
|
||||
const [showRentalModal, setShowRentalModal] = useState<{ obj: typeof RENTAL_OBJECTS[0]; date: string } | null>(null)
|
||||
const [search, setSearch] = useState('')
|
||||
const [statusFilter, setStatusFilter] = useState<BookingStatus | 'all'>('all')
|
||||
const [selected, setSelected] = useState<Booking | null>(null)
|
||||
@@ -103,6 +108,12 @@ export function BookingsPage() {
|
||||
<span className="hidden sm:inline">Новое</span>
|
||||
</button>
|
||||
)}
|
||||
{tab === 'rental' && (
|
||||
<button onClick={() => setNewRentalStep('pick')} className="btn-primary">
|
||||
<Plus size={15} />
|
||||
<span className="hidden sm:inline">Новая аренда</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
@@ -359,6 +370,57 @@ export function BookingsPage() {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* New rental — step 1: pick object + date */}
|
||||
{newRentalStep === 'pick' && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50">
|
||||
<div className="bg-white dark:bg-slate-800 rounded-2xl shadow-2xl w-full max-w-sm p-6 space-y-4">
|
||||
<h2 className="font-semibold text-slate-900 dark:text-slate-100 text-lg">Новая аренда</h2>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Объект</label>
|
||||
<select className="input" value={rentalPickObj} onChange={e => setRentalPickObj(e.target.value)}>
|
||||
{RENTAL_OBJECTS.map(o => (
|
||||
<option key={o.id} value={o.id}>{o.icon} {o.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Дата</label>
|
||||
<input
|
||||
type="date" className="input"
|
||||
value={rentalPickDate}
|
||||
onChange={e => setRentalPickDate(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 justify-end">
|
||||
<button onClick={() => setNewRentalStep('idle')} className="btn-secondary">Отмена</button>
|
||||
<button
|
||||
className="btn-primary"
|
||||
onClick={() => {
|
||||
const obj = RENTAL_OBJECTS.find(o => o.id === rentalPickObj)
|
||||
if (obj) { setShowRentalModal({ obj, date: rentalPickDate }); setNewRentalStep('idle') }
|
||||
}}
|
||||
>
|
||||
Далее →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* New rental — step 2: booking form */}
|
||||
{showRentalModal && (
|
||||
<RentalBookingModal
|
||||
obj={showRentalModal.obj}
|
||||
date={showRentalModal.date}
|
||||
existingBookings={rentalBookings.filter(b => b.objectId === showRentalModal.obj.id && b.date === showRentalModal.date)}
|
||||
onClose={() => setShowRentalModal(null)}
|
||||
onSave={(b) => {
|
||||
setRentalBookings(prev => [...prev, b])
|
||||
setShowRentalModal(null)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user