- Add AvailabilityPage with price grid (categories × dates), drag-select, channel prices, EditPanel, PeriodModal, and rate periods tab - Add ModulesContext with localStorage persistence and dynamic sidebar items - Add ModulesPage with WiFi Auth, Payments, TV Welcome, Smart Locks, OLAP Reports, Website Builder, Booking Widget modules - Add ReportsPage (OLAP analytics with KPI cards, charts, tables) - Add WebsitePage and BookingWidgetPage placeholders - Remove hotel slug from URL routing; hotel context from JWT - Add Доступность nav item in Sidebar under Управление - Fix LoginPage redirects and HotelSync branding Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
135 lines
6.6 KiB
TypeScript
135 lines
6.6 KiB
TypeScript
import { BarChart3, TrendingUp, Users, BedDouble, ArrowUpRight, ArrowDownRight } from 'lucide-react'
|
||
import { cn } from '../lib/utils'
|
||
|
||
const KPI = [
|
||
{ label: 'Загрузка (OCC)', value: '73.4%', delta: '+5.2%', up: true },
|
||
{ label: 'ADR', value: '₽ 4 820', delta: '+8.1%', up: true },
|
||
{ label: 'RevPAR', value: '₽ 3 538', delta: '+14.3%', up: true },
|
||
{ label: 'Отменённые', value: '6.2%', delta: '-1.4%', up: true },
|
||
]
|
||
|
||
const CHANNEL_DATA = [
|
||
{ name: 'Прямые', pct: 38, color: 'bg-brand-500', revenue: '₽ 412 000' },
|
||
{ name: 'Booking.com', pct: 31, color: 'bg-blue-500', revenue: '₽ 336 200' },
|
||
{ name: 'Airbnb', pct: 18, color: 'bg-rose-500', revenue: '₽ 195 100' },
|
||
{ name: 'Другие', pct: 13, color: 'bg-slate-400', revenue: '₽ 140 800' },
|
||
]
|
||
|
||
const MONTHS = ['Окт', 'Ноя', 'Дек', 'Янв', 'Фев', 'Мар']
|
||
const OCC = [61, 68, 79, 65, 70, 73]
|
||
const maxOcc = Math.max(...OCC)
|
||
|
||
export function ReportsPage() {
|
||
return (
|
||
<div className="p-6 max-w-5xl mx-auto space-y-6">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-10 h-10 rounded-xl bg-cyan-600 flex items-center justify-center shrink-0">
|
||
<BarChart3 size={20} className="text-white" />
|
||
</div>
|
||
<div>
|
||
<h1 className="text-xl font-bold text-slate-900 dark:text-slate-100">Аналитика</h1>
|
||
<p className="text-sm text-slate-500 dark:text-slate-400">Март 2026 • Grand Palace Hotel</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* KPI cards */}
|
||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
|
||
{KPI.map(k => (
|
||
<div key={k.label} className="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4">
|
||
<p className="text-xs text-slate-500 dark:text-slate-400 mb-2">{k.label}</p>
|
||
<p className="text-2xl font-bold text-slate-900 dark:text-slate-100">{k.value}</p>
|
||
<div className={cn('flex items-center gap-1 text-xs font-medium mt-1', k.up ? 'text-emerald-600' : 'text-red-500')}>
|
||
{k.up ? <ArrowUpRight size={13} /> : <ArrowDownRight size={13} />}
|
||
{k.delta} vs прошлый месяц
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||
{/* Occupancy chart */}
|
||
<div className="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-5">
|
||
<div className="flex items-center gap-2 mb-4">
|
||
<TrendingUp size={16} className="text-brand-600" />
|
||
<h3 className="font-semibold text-slate-900 dark:text-slate-100 text-sm">Загрузка по месяцам</h3>
|
||
</div>
|
||
<div className="flex items-end gap-2 h-32">
|
||
{MONTHS.map((m, i) => (
|
||
<div key={m} className="flex-1 flex flex-col items-center gap-1">
|
||
<span className="text-xs font-medium text-slate-700 dark:text-slate-300">{OCC[i]}%</span>
|
||
<div
|
||
className="w-full rounded-t-md bg-brand-500 dark:bg-brand-600 transition-all"
|
||
style={{ height: `${(OCC[i] / maxOcc) * 100}%` }}
|
||
/>
|
||
<span className="text-xs text-slate-400">{m}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Channel breakdown */}
|
||
<div className="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-5">
|
||
<div className="flex items-center gap-2 mb-4">
|
||
<Users size={16} className="text-brand-600" />
|
||
<h3 className="font-semibold text-slate-900 dark:text-slate-100 text-sm">Источники бронирований</h3>
|
||
</div>
|
||
<div className="space-y-3">
|
||
{CHANNEL_DATA.map(c => (
|
||
<div key={c.name}>
|
||
<div className="flex items-center justify-between text-xs mb-1">
|
||
<span className="text-slate-700 dark:text-slate-300 font-medium">{c.name}</span>
|
||
<span className="text-slate-500 dark:text-slate-400">{c.revenue}</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<div className="flex-1 h-2 bg-slate-100 dark:bg-slate-700 rounded-full overflow-hidden">
|
||
<div className={cn('h-full rounded-full', c.color)} style={{ width: `${c.pct}%` }} />
|
||
</div>
|
||
<span className="text-xs text-slate-500 w-8 text-right">{c.pct}%</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Room stats */}
|
||
<div className="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-5">
|
||
<div className="flex items-center gap-2 mb-4">
|
||
<BedDouble size={16} className="text-brand-600" />
|
||
<h3 className="font-semibold text-slate-900 dark:text-slate-100 text-sm">Топ номеров по выручке</h3>
|
||
</div>
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full text-sm">
|
||
<thead>
|
||
<tr className="text-left text-xs text-slate-400 uppercase border-b border-slate-100 dark:border-slate-700">
|
||
<th className="pb-2 pr-4 font-medium">Номер</th>
|
||
<th className="pb-2 pr-4 font-medium">Тип</th>
|
||
<th className="pb-2 pr-4 font-medium">Загрузка</th>
|
||
<th className="pb-2 font-medium">Выручка</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-slate-100 dark:divide-slate-700">
|
||
{[
|
||
{ n: '401', t: 'Penthouse', occ: '89%', rev: '₽ 96 200' },
|
||
{ n: '202', t: 'Suite', occ: '84%', rev: '₽ 74 800' },
|
||
{ n: '302', t: 'Junior Suite', occ: '81%', rev: '₽ 55 100' },
|
||
{ n: '201', t: 'Deluxe', occ: '78%', rev: '₽ 40 600' },
|
||
{ n: '101', t: 'Standard', occ: '71%', rev: '₽ 24 900' },
|
||
].map(r => (
|
||
<tr key={r.n}>
|
||
<td className="py-2.5 pr-4 font-medium text-slate-900 dark:text-slate-100">{r.n}</td>
|
||
<td className="py-2.5 pr-4 text-slate-500">{r.t}</td>
|
||
<td className="py-2.5 pr-4">
|
||
<span className="text-emerald-600 font-medium">{r.occ}</span>
|
||
</td>
|
||
<td className="py-2.5 font-semibold text-slate-900 dark:text-slate-100">{r.rev}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|