Fix require_cap TypeError, rewrite /analysis/comparison, add room category sync

- require_cap was a Depends-factory but every call site uses it inline;
  make it an inline checker (fixes 500 on /analysis/hotels, /direct/*)
- /analysis/comparison returned a per-date matrix the frontend never read;
  return per-hotel aggregates (our/their avg, price index) and default to
  all active competitors so the Market Comparison table works without params
- Room categories were never populated (lost in port): add sites_list fetch
  to the Newbook client, categories list/sync/toggle endpoints, and a
  Settings card — without included categories every rates sync exits early

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-05 13:47:10 +00:00
parent c9f2dc804c
commit 69c9b16a2b
6 changed files with 5787 additions and 52 deletions

5509
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
import { useState } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Save, RefreshCw, Database, Clock } from 'lucide-react'
import { Save, RefreshCw, Database, Clock, BedDouble } from 'lucide-react'
import api from '../api'
const TABS = [
@ -13,6 +13,15 @@ interface SystemConfig {
[key: string]: string | null
}
interface RoomCategory {
id: number
site_id: string
site_name: string
room_count: number
is_included: boolean
display_order: number
}
export default function Settings() {
const { tab: tabParam } = useParams<{ tab?: string }>()
const navigate = useNavigate()
@ -167,6 +176,95 @@ function NewbookTab({ config, isLoading, onSave, onSyncNow, saving, syncing }: N
</div>
</div>
</div>
<RoomCategoriesCard />
</div>
)
}
// ─── Room Categories ──────────────────────────────────────────────────────────
function RoomCategoriesCard() {
const qc = useQueryClient()
const { data: categories, isLoading } = useQuery<RoomCategory[]>({
queryKey: ['room-categories'],
queryFn: () => api.get('/bookability/categories').then(r => r.data),
})
const syncCategories = useMutation({
mutationFn: () => api.post('/bookability/categories/sync'),
onSuccess: () => qc.invalidateQueries({ queryKey: ['room-categories'] }),
})
const toggleCategory = useMutation({
mutationFn: ({ id, included }: { id: number; included: boolean }) =>
api.patch(`/bookability/categories/${id}`, { is_included: included }),
onSuccess: () => qc.invalidateQueries({ queryKey: ['room-categories'] }),
})
return (
<div className="card">
<div className="card-header">
<span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<BedDouble size={16} strokeWidth={1.75} />
Room Categories
</span>
<button
className="btn btn-outline btn-sm"
onClick={() => syncCategories.mutate()}
disabled={syncCategories.isPending}
>
<RefreshCw size={13} strokeWidth={1.75} />
{syncCategories.isPending ? 'Fetching…' : 'Fetch from Newbook'}
</button>
</div>
<div className="card-body" style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<p style={{ fontSize: 13, color: 'var(--text-mid)', margin: 0 }}>
Rate syncs only fetch tariffs for the included categories below. If the list is
empty, fetch categories from Newbook first otherwise syncs will do nothing.
</p>
{syncCategories.isError && (
<p style={{ fontSize: 13, color: 'var(--danger)', margin: 0 }}>
{(syncCategories.error as any)?.response?.data?.detail || 'Category fetch failed'}
</p>
)}
{isLoading ? (
<div className="loading-state"><div className="spinner" /> Loading</div>
) : !categories?.length ? (
<p style={{ fontSize: 13, color: 'var(--warning)', margin: 0 }}>
No room categories yet click Fetch from Newbook to load them.
</p>
) : (
<div className="table-wrap">
<table>
<thead>
<tr>
<th>Included</th>
<th>Category</th>
<th>Rooms</th>
</tr>
</thead>
<tbody>
{categories.map(cat => (
<tr key={cat.id}>
<td>
<input
type="checkbox"
checked={cat.is_included}
onChange={e => toggleCategory.mutate({ id: cat.id, included: e.target.checked })}
disabled={toggleCategory.isPending}
/>
</td>
<td>{cat.site_name}</td>
<td>{cat.room_count}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
)
}