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:
2026-03-11 20:17:40 +03:00
parent 0e0c11785f
commit fe76fc2f50
6 changed files with 1698 additions and 1 deletions

View File

@@ -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>
)
}