Files
hotelsync/src/pages/ReportsPage.tsx
HotelSync b277e3e38d Add Availability Calendar, Modules system, and route refactoring
- 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>
2026-03-11 14:43:18 +03:00

135 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
)
}