feat: persist rate overrides to DB when applying prices in AvailabilityPage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-23 16:14:45 +03:00
parent 8312d2372e
commit e5daf91ed4
5 changed files with 194 additions and 34 deletions

View File

@@ -20,7 +20,7 @@ interface AvailabilityCat {
function buildPriceGrid(
categories: AvailabilityCat[],
days = 60,
days = 90,
): Record<string, Record<string, PriceCell>> {
const result: Record<string, Record<string, PriceCell>> = {}
const today = new Date()
@@ -29,13 +29,11 @@ function buildPriceGrid(
for (let i = 0; i < days; i++) {
const d = addDays(today, i)
const dateStr = format(d, 'yyyy-MM-dd')
const isWeekend = [5, 6].includes(getDay(d))
const basePrice = Math.round(cat.basePrice * (isWeekend ? 1.25 : 1.0))
const channelPrices: Record<string, number> = {}
for (const ch of RATE_CHANNELS) {
channelPrices[ch.id] = Math.round(basePrice * (DEFAULT_CHANNEL_MARKUP[ch.id] ?? 1))
channelPrices[ch.id] = Math.round(cat.basePrice * (DEFAULT_CHANNEL_MARKUP[ch.id] ?? 1))
}
result[cat.id][dateStr] = { price: basePrice, extraPerson: 0, minNights: 1, channelPrices, closed: false }
result[cat.id][dateStr] = { price: cat.basePrice, extraPerson: 0, minNights: 1, channelPrices, closed: false }
}
}
return result
@@ -684,63 +682,65 @@ export function AvailabilityPage() {
const [pickerDate, setPickerDate] = useState(format(new Date(), DATE_FMT))
const navPickerRef = useRef<HTMLDivElement>(null)
// Load categories, rooms, and rate periods from API
// Load categories, rooms, rate periods and overrides from API
useEffect(() => {
if (!slug) return
setLoading(true)
Promise.all([
api.categories.list(slug),
api.rooms.list(slug),
api.ratePeriods.list(slug),
]).then(([cats, rooms, apiPeriods]) => {
api.categories.list(slug).catch(() => []),
api.rooms.list(slug).catch(() => []),
api.ratePeriods.list(slug).catch(() => []),
api.rateOverrides.list(slug).catch(() => []),
]).then(([cats, rooms, apiPeriods, apiOverrides]) => {
// Build AvailabilityCat[] from real categories, derive basePrice from rooms
const availCats: AvailabilityCat[] = cats.map(cat => {
const catRooms = rooms.filter(r => r.categoryId === cat.id)
const basePrice = catRooms.length > 0
? Math.min(...catRooms.map(r => r.baseRate))
: 3000
return {
id: cat.id,
name: cat.name,
color: cat.color || '#6366f1',
basePrice,
}
return { id: cat.id, name: cat.name, color: cat.color || '#6366f1', basePrice }
})
// If no categories defined, fall back to room types as groups
// If no categories, fall back to room types as groups
const colors = ['#6366f1','#10b981','#f59e0b','#ef4444','#8b5cf6','#06b6d4']
const finalCats = availCats.length > 0 ? availCats : (() => {
const typeMap = new Map<string, number>()
for (const r of rooms) {
if (!r.type) continue
if (!typeMap.has(r.type)) typeMap.set(r.type, r.baseRate)
else typeMap.set(r.type, Math.min(typeMap.get(r.type)!, r.baseRate))
}
const colors = ['#6366f1','#10b981','#f59e0b','#ef4444','#8b5cf6','#06b6d4']
return Array.from(typeMap.entries()).map(([name, price], i) => ({
id: name.toLowerCase().replace(/\s+/g, '_'),
name,
color: colors[i % colors.length],
basePrice: price,
name, color: colors[i % colors.length], basePrice: price,
}))
})()
setRoomCategories(finalCats)
setPrices(buildPriceGrid(finalCats))
// Map API rate periods to local RatePeriod format
// Build initial grid then apply saved overrides on top
const grid = buildPriceGrid(finalCats)
for (const o of apiOverrides) {
if (!grid[o.category_id]) continue
grid[o.category_id][o.date] = {
price: o.price,
extraPerson: o.extra_person,
minNights: o.min_nights,
channelPrices: o.channel_prices,
closed: o.closed,
}
}
setPrices(grid)
// Map API rate periods
const mappedPeriods: RatePeriod[] = apiPeriods.map(p => ({
id: p.id,
name: p.name,
startDate: p.startDate,
endDate: p.endDate,
notes: p.notes ?? undefined,
categoryPrices: p.categoryPrices,
channelMarkup: p.channelMarkup,
extraPersonPrice: p.extraPersonPrice,
minNights: p.minNights,
daysOfWeek: p.daysOfWeek ?? undefined,
id: p.id, name: p.name, startDate: p.startDate, endDate: p.endDate,
notes: p.notes ?? undefined, categoryPrices: p.categoryPrices,
channelMarkup: p.channelMarkup, extraPersonPrice: p.extraPersonPrice,
minNights: p.minNights, daysOfWeek: p.daysOfWeek ?? undefined,
}))
setPeriods(mappedPeriods)
}).catch(console.error).finally(() => setLoading(false))
}).finally(() => setLoading(false))
}, [slug])
useEffect(() => {
@@ -797,6 +797,13 @@ export function AvailabilityPage() {
const catsToUpdate = onlyCategoryId
? roomCategories.filter(c => c.id === onlyCategoryId)
: roomCategories
const overrides: Array<{
category_id: string; date: string; price: number
extra_person: number; min_nights: number
channel_prices: Record<string, number>; closed: boolean
}> = []
setPrices(prev => {
const next = { ...prev }
for (const cat of catsToUpdate) {
@@ -810,10 +817,26 @@ export function AvailabilityPage() {
next[cat.id][d] = {
price: basePrice, extraPerson, minNights, channelPrices, closed,
}
overrides.push({
category_id: cat.id,
date: d,
price: basePrice,
extra_person: extraPerson,
min_nights: minNights,
channel_prices: channelPrices,
closed,
})
}
}
return next
})
if (overrides.length > 0) {
api.rateOverrides.bulkUpsert(slug, overrides).catch(err =>
console.error('Failed to save rate overrides:', err)
)
}
setShowEditPanel(false)
setSelection(null)
if (onlyCategoryId) setSelectedCat(null)