feat: hourly pricing + category base prices in booking modal + availability grid
- Add hourly_price column to rate_overrides, base_price/allow_hourly/hourly_base_price to room_categories (migration 033) - Update categories and rate-overrides backend routes to handle new fields - AvailabilityPage: hourly/nightly mode toggle, period prices auto-applied to grid with 'П' indicator, confirmation dialog when overriding period prices - BookingModal: nightly/hourly toggle at top, pricing hierarchy (override → category base → room rate) - RoomCategoriesPage: base_price/allow_hourly/hourly_base_price fields in category form; inline rooms panel - CalendarPage + BookingCalendar: pass hourlyPriceOverrides and categoryBasePrices down to BookingModal Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,7 @@ const categories: FastifyPluginAsync = async (fastify) => {
|
||||
fastify.post<SlugParam & { Body: {
|
||||
name: string; description?: string; color?: string
|
||||
amenities?: string[]; photos?: string[]; sort_order?: number
|
||||
base_price?: number; allow_hourly?: boolean; hourly_base_price?: number
|
||||
} }>(
|
||||
'/api/hotels/:slug/categories',
|
||||
{ onRequest: [fastify.authenticate] },
|
||||
@@ -44,11 +45,17 @@ const categories: FastifyPluginAsync = async (fastify) => {
|
||||
return reply.code(403).send({ error: 'Forbidden' })
|
||||
const hotelId = await getHotelId(slug)
|
||||
if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' })
|
||||
const { name, description = '', color = '#4F46E5', amenities = [], photos = [], sort_order = 0 } = request.body
|
||||
const {
|
||||
name, description = '', color = '#4F46E5', amenities = [], photos = [], sort_order = 0,
|
||||
base_price = 0, allow_hourly = false, hourly_base_price = 0,
|
||||
} = request.body
|
||||
const { rows } = await db.query(
|
||||
`INSERT INTO room_categories (hotel_id, name, description, color, amenities, photos, sort_order)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING *`,
|
||||
[hotelId, name, description, color, amenities, photos, sort_order],
|
||||
`INSERT INTO room_categories
|
||||
(hotel_id, name, description, color, amenities, photos, sort_order,
|
||||
base_price, allow_hourly, hourly_base_price)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) RETURNING *`,
|
||||
[hotelId, name, description, color, amenities, photos, sort_order,
|
||||
base_price, allow_hourly, hourly_base_price],
|
||||
)
|
||||
return reply.code(201).send(rows[0])
|
||||
},
|
||||
@@ -58,6 +65,7 @@ const categories: FastifyPluginAsync = async (fastify) => {
|
||||
fastify.patch<SlugIdParam & { Body: {
|
||||
name?: string; description?: string; color?: string
|
||||
amenities?: string[]; photos?: string[]; sort_order?: number
|
||||
base_price?: number; allow_hourly?: boolean; hourly_base_price?: number
|
||||
} }>(
|
||||
'/api/hotels/:slug/categories/:id',
|
||||
{ onRequest: [fastify.authenticate] },
|
||||
@@ -71,12 +79,15 @@ const categories: FastifyPluginAsync = async (fastify) => {
|
||||
const sets: string[] = ['updated_at = NOW()']
|
||||
const vals: unknown[] = [hotelId, id]
|
||||
let i = 3
|
||||
if (b.name !== undefined) { sets.push(`name = $${i++}`); vals.push(b.name) }
|
||||
if (b.description !== undefined) { sets.push(`description = $${i++}`); vals.push(b.description) }
|
||||
if (b.color !== undefined) { sets.push(`color = $${i++}`); vals.push(b.color) }
|
||||
if (b.amenities !== undefined) { sets.push(`amenities = $${i++}`); vals.push(b.amenities) }
|
||||
if (b.photos !== undefined) { sets.push(`photos = $${i++}`); vals.push(b.photos) }
|
||||
if (b.sort_order !== undefined) { sets.push(`sort_order = $${i++}`); vals.push(b.sort_order) }
|
||||
if (b.name !== undefined) { sets.push(`name = $${i++}`); vals.push(b.name) }
|
||||
if (b.description !== undefined) { sets.push(`description = $${i++}`); vals.push(b.description) }
|
||||
if (b.color !== undefined) { sets.push(`color = $${i++}`); vals.push(b.color) }
|
||||
if (b.amenities !== undefined) { sets.push(`amenities = $${i++}`); vals.push(b.amenities) }
|
||||
if (b.photos !== undefined) { sets.push(`photos = $${i++}`); vals.push(b.photos) }
|
||||
if (b.sort_order !== undefined) { sets.push(`sort_order = $${i++}`); vals.push(b.sort_order) }
|
||||
if (b.base_price !== undefined) { sets.push(`base_price = $${i++}`); vals.push(b.base_price) }
|
||||
if (b.allow_hourly !== undefined) { sets.push(`allow_hourly = $${i++}`); vals.push(b.allow_hourly) }
|
||||
if (b.hourly_base_price !== undefined) { sets.push(`hourly_base_price = $${i++}`); vals.push(b.hourly_base_price) }
|
||||
const { rows } = await db.query(
|
||||
`UPDATE room_categories SET ${sets.join(', ')} WHERE hotel_id=$1 AND id=$2 RETURNING *`,
|
||||
vals,
|
||||
|
||||
@@ -24,7 +24,7 @@ const rateOverrides: FastifyPluginAsync = async (fastify) => {
|
||||
if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' })
|
||||
const { rows } = await db.query(
|
||||
`SELECT category_id, to_char(date, 'YYYY-MM-DD') AS date,
|
||||
price, extra_person, min_nights, channel_prices, closed
|
||||
price, extra_person, min_nights, channel_prices, closed, hourly_price
|
||||
FROM rate_overrides
|
||||
WHERE hotel_id = $1
|
||||
ORDER BY date, category_id`,
|
||||
@@ -40,6 +40,7 @@ const rateOverrides: FastifyPluginAsync = async (fastify) => {
|
||||
category_id: string; date: string; price: number
|
||||
extra_person?: number; min_nights?: number
|
||||
channel_prices?: Record<string, number>; closed?: boolean
|
||||
hourly_price?: number | null
|
||||
}>
|
||||
} }>(
|
||||
'/api/hotels/:slug/rate-overrides/bulk',
|
||||
@@ -54,13 +55,11 @@ const rateOverrides: FastifyPluginAsync = async (fastify) => {
|
||||
const { overrides } = request.body
|
||||
if (!overrides?.length) return { count: 0 }
|
||||
|
||||
// Build parameterized bulk upsert:
|
||||
// $1 = hotelId, then per row: category_id, date, price, extra_person, min_nights, channel_prices, closed
|
||||
const vals: unknown[] = [hotelId]
|
||||
const rowPlaceholders: string[] = []
|
||||
let idx = 2
|
||||
for (const o of overrides) {
|
||||
rowPlaceholders.push(`($1, $${idx}, $${idx+1}, $${idx+2}, $${idx+3}, $${idx+4}, $${idx+5}, $${idx+6})`)
|
||||
rowPlaceholders.push(`($1, $${idx}, $${idx+1}, $${idx+2}, $${idx+3}, $${idx+4}, $${idx+5}, $${idx+6}, $${idx+7})`)
|
||||
vals.push(
|
||||
o.category_id,
|
||||
o.date,
|
||||
@@ -69,13 +68,14 @@ const rateOverrides: FastifyPluginAsync = async (fastify) => {
|
||||
o.min_nights ?? 1,
|
||||
JSON.stringify(o.channel_prices ?? {}),
|
||||
o.closed ?? false,
|
||||
o.hourly_price ?? null,
|
||||
)
|
||||
idx += 7
|
||||
idx += 8
|
||||
}
|
||||
|
||||
await db.query(
|
||||
`INSERT INTO rate_overrides
|
||||
(hotel_id, category_id, date, price, extra_person, min_nights, channel_prices, closed)
|
||||
(hotel_id, category_id, date, price, extra_person, min_nights, channel_prices, closed, hourly_price)
|
||||
VALUES ${rowPlaceholders.join(', ')}
|
||||
ON CONFLICT (hotel_id, category_id, date) DO UPDATE SET
|
||||
price = EXCLUDED.price,
|
||||
@@ -83,6 +83,7 @@ const rateOverrides: FastifyPluginAsync = async (fastify) => {
|
||||
min_nights = EXCLUDED.min_nights,
|
||||
channel_prices = EXCLUDED.channel_prices,
|
||||
closed = EXCLUDED.closed,
|
||||
hourly_price = COALESCE(EXCLUDED.hourly_price, rate_overrides.hourly_price),
|
||||
updated_at = NOW()`,
|
||||
vals,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user