Add Rate Monitor app — Booking.com + direct booking engine competitor rates
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>
This commit is contained in:
commit
e05054172f
50 changed files with 11860 additions and 0 deletions
70
frontend/src/components/Layout.tsx
Normal file
70
frontend/src/components/Layout.tsx
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
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>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue