Files
hotelsync/src/pages/AdminDashboard.tsx
HotelSync 55a34dc0dc Connect ChannelsPage, FloorMapPage, SettingsPage, UsersPage to real API
- ChannelsPage: load from API, toggle/sync call real endpoints; normalize
  enabled→isEnabled, lastSyncedAt→lastSyncAt, add displayName/mappings defaults
- FloorMapPage: load rooms+bookings from API; create booking via API
- SettingsPage: load hotel via GET /api/hotels/:slug; save general section
  via PATCH (name, address, timezone, currency, check_in/out times)
- UsersPage: load users from API; create/update/delete via API;
  map backend User (name/role) → StaffUser (firstName/lastName/StaffRole)
- api.ts: add hotels.get/update, users.delete, channel normalization,
  HotelPayload/toHotelPayload
- types/index.ts: extend Hotel with phone/checkInTime/checkOutTime/optional fields;
  add User.createdAt/updatedAt
- backend/routes/hotels.ts: extend PATCH to allow address/phone/check_in_time/check_out_time
- backend/migrations/004_hotels_contacts.sql: add address/phone/check_in/out_time to hotels

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 14:21:45 +03:00

120 lines
6.0 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 { Building2, Users, TrendingUp, DollarSign, CheckCircle2, AlertCircle, Plus } from 'lucide-react'
import { MOCK_HOTELS, MOCK_USERS } from '../data/mockData'
import { cn, PLAN_COLORS, PLAN_LABELS } from '../lib/utils'
import { Badge } from '../components/ui/Badge'
export function AdminDashboard() {
const activeHotels = MOCK_HOTELS.filter(h => h.isActive).length
const totalRooms = MOCK_HOTELS.reduce((s, h) => s + (h.roomCount ?? 0), 0)
return (
<div className="p-4 md:p-6 space-y-6">
{/* Header */}
<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">HotelSync SaaS · Обзор платформы</p>
</div>
{/* Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{[
{ label: 'Всего отелей', value: MOCK_HOTELS.length, icon: Building2, color: 'text-brand-600 dark:text-brand-400', bg: 'bg-brand-50 dark:bg-brand-900/20' },
{ label: 'Активных', value: activeHotels, icon: CheckCircle2, color: 'text-emerald-600 dark:text-emerald-400', bg: 'bg-emerald-50 dark:bg-emerald-900/20' },
{ label: 'Всего номеров', value: totalRooms, icon: TrendingUp, color: 'text-amber-600 dark:text-amber-400', bg: 'bg-amber-50 dark:bg-amber-900/20' },
{ label: 'Пользователей', value: MOCK_USERS.length, icon: Users, color: 'text-violet-600 dark:text-violet-400', bg: 'bg-violet-50 dark:bg-violet-900/20' },
].map(s => {
const Icon = s.icon
return (
<div key={s.label} className={cn('card p-4 border-0', s.bg)}>
<div className="flex items-center justify-between mb-2">
<Icon size={18} className={s.color} />
</div>
<p className={cn('text-2xl font-bold', s.color)}>{s.value}</p>
<p className="text-sm text-slate-600 dark:text-slate-400">{s.label}</p>
</div>
)
})}
</div>
{/* Revenue by plan */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div className="card p-5">
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-slate-900 dark:text-slate-100">Отели</h3>
<button className="btn-primary py-1.5 text-xs">
<Plus size={13} />
Добавить
</button>
</div>
<div className="space-y-3">
{MOCK_HOTELS.map(hotel => (
<div key={hotel.id} className="flex items-center gap-3 p-3 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700/40 transition-colors cursor-pointer">
<div className={cn(
'w-8 h-8 rounded-lg flex items-center justify-center text-sm font-bold',
hotel.isActive
? 'bg-brand-100 dark:bg-brand-900/30 text-brand-700 dark:text-brand-300'
: 'bg-slate-100 dark:bg-slate-700 text-slate-500',
)}>
{hotel.name.charAt(0)}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-slate-900 dark:text-slate-100 truncate">{hotel.name}</p>
<p className="text-xs text-slate-500 dark:text-slate-400">{hotel.address} · {hotel.roomCount} номеров</p>
</div>
<div className="flex items-center gap-2 shrink-0">
<Badge className={PLAN_COLORS[hotel.plan]}>{PLAN_LABELS[hotel.plan]}</Badge>
<div className={cn('w-2 h-2 rounded-full', hotel.isActive ? 'bg-emerald-500' : 'bg-slate-400')} />
</div>
</div>
))}
</div>
</div>
<div className="card p-5">
<h3 className="font-semibold text-slate-900 dark:text-slate-100 mb-4">Статистика по тарифам</h3>
<div className="space-y-3">
{(['starter', 'pro', 'enterprise'] as const).map(plan => {
const count = MOCK_HOTELS.filter(h => h.plan === plan).length
const pct = Math.round((count / MOCK_HOTELS.length) * 100)
return (
<div key={plan}>
<div className="flex items-center justify-between mb-1">
<Badge className={PLAN_COLORS[plan]}>{PLAN_LABELS[plan]}</Badge>
<span className="text-sm font-medium text-slate-700 dark:text-slate-300">{count} отелей ({pct}%)</span>
</div>
<div className="h-2 rounded-full bg-slate-200 dark:bg-slate-700">
<div
className="h-2 rounded-full bg-brand-500 transition-all"
style={{ width: `${pct}%` }}
/>
</div>
</div>
)
})}
</div>
<div className="mt-6 pt-5 border-t border-slate-200 dark:border-slate-700">
<h4 className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3">Пользователи</h4>
<div className="space-y-2">
{MOCK_USERS.map(u => (
<div key={u.id} className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-brand-600 flex items-center justify-center text-white text-sm font-semibold shrink-0">
{u.name.charAt(0)}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-slate-900 dark:text-slate-100">{u.name}</p>
<p className="text-xs text-slate-500 dark:text-slate-400">{u.email}</p>
</div>
<Badge className="bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300 text-[10px]">
{u.role}
</Badge>
</div>
))}
</div>
</div>
</div>
</div>
</div>
)
}