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:
parent
c9f2dc804c
commit
69c9b16a2b
6 changed files with 5787 additions and 52 deletions
5509
frontend/package-lock.json
generated
Normal file
5509
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue