Combines Booking.com Playwright scraper (from forecasting), direct booking engine scraper (ported from laptop-archive/guestline-monitor), and Newbook own-hotel rates into one focused tool. Four views: Bookability, Market View (with price index badges + direct rate sub-rows), Direct Rates (per-competitor room breakdown, min-stay flags, hotel config/discovery), Rate Analysis (advance purchase curve, DOW chart, rate timeline, comparison table). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
70 lines
2.6 KiB
TypeScript
70 lines
2.6 KiB
TypeScript
import { NavLink } from 'react-router-dom'
|
|
import { TrendingUp, Calendar, Globe, BarChart2, Settings, Building2 } from 'lucide-react'
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import { useAuth } from './AuthGate'
|
|
import { can } from '../types'
|
|
import api from '../api'
|
|
import type { ReactNode } from 'react'
|
|
|
|
const ICON = { size: 16, strokeWidth: 1.75 }
|
|
|
|
const NAV = [
|
|
{ to: '/bookability', label: 'Bookability', icon: Calendar, cap: 'view_own_rates' },
|
|
{ to: '/market', label: 'Market View', icon: Globe, cap: 'view_competitors' },
|
|
{ to: '/direct', label: 'Direct Rates', icon: Building2, cap: 'view_direct_rates' },
|
|
{ to: '/analysis', label: 'Rate Analysis', icon: TrendingUp, cap: 'rate_analysis' },
|
|
{ to: '/settings', label: 'Settings', icon: Settings, cap: 'manage_scraper' },
|
|
]
|
|
|
|
export default function Layout({ children }: { children: ReactNode }) {
|
|
const { user } = useAuth()
|
|
const items = NAV.filter(n => can(user, n.cap))
|
|
|
|
const { data: alertCount } = useQuery<number>({
|
|
queryKey: ['parity-alert-count'],
|
|
queryFn: () => api.get('/competitors/parity/alerts?status=active').then(r => r.data.length),
|
|
refetchInterval: 60_000,
|
|
enabled: can(user, 'view_competitors'),
|
|
})
|
|
|
|
return (
|
|
<div className="app-shell">
|
|
<aside className="sidebar">
|
|
<div className="sidebar-logo">
|
|
<BarChart2 size={18} strokeWidth={1.75} />
|
|
Rate Monitor
|
|
</div>
|
|
<nav className="sidebar-nav">
|
|
{items.map(({ to, label, icon: Icon }) => (
|
|
<NavLink key={to} to={to} className={({ isActive }) => isActive ? 'active' : ''}>
|
|
<Icon {...ICON} />
|
|
{label}
|
|
{to === '/market' && alertCount ? (
|
|
<span style={{
|
|
marginLeft: 'auto', background: 'var(--danger)', color: '#fff',
|
|
borderRadius: 10, fontSize: 10, fontWeight: 700,
|
|
padding: '1px 6px', lineHeight: '16px',
|
|
}}>{alertCount}</span>
|
|
) : null}
|
|
</NavLink>
|
|
))}
|
|
</nav>
|
|
<div className="sidebar-user">{user.name || user.email}</div>
|
|
</aside>
|
|
|
|
<header className="top-bar">
|
|
<BarChart2 size={18} strokeWidth={1.75} color="var(--gold)" />
|
|
<span className="top-bar-title">Rate Monitor</span>
|
|
<nav className="top-bar-nav">
|
|
{items.map(({ to, label }) => (
|
|
<NavLink key={to} to={to} className={({ isActive }) => isActive ? 'active' : ''}>
|
|
{label}
|
|
</NavLink>
|
|
))}
|
|
</nav>
|
|
</header>
|
|
|
|
<main className="page-content">{children}</main>
|
|
</div>
|
|
)
|
|
}
|