feat(logistics): hotels CRUD + rooming + assignment procedures + travel email
Replace getHotel/upsertHotel with listHotels/createHotel/updateHotel/deleteHotel (multi-hotel per edition). Add listRooming, assignStay, assignTeamToHotel, and unassignStay procedures for per-attendee room assignments. Update setFlightStatus to include attendee's HotelStay in TRAVEL_CONFIRMED notification metadata. Extend getTravelConfirmedTemplate to render room number and check-in/out dates. All procedures are adminProcedure and audit-logged. 10 new unit tests green. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2297,6 +2297,11 @@ function getTravelConfirmedTemplate(
|
||||
address?: string | null
|
||||
link?: string | null
|
||||
},
|
||||
room?: {
|
||||
roomNumber?: string | null
|
||||
checkInAt?: string | null
|
||||
checkOutAt?: string | null
|
||||
},
|
||||
): EmailTemplate {
|
||||
const greeting = name ? `Hi ${name},` : 'Hi there,'
|
||||
|
||||
@@ -2333,18 +2338,38 @@ function getTravelConfirmedTemplate(
|
||||
`</ul>`
|
||||
: ''
|
||||
|
||||
const fmtDateShort = (d: string | null | undefined) => {
|
||||
if (!d) return null
|
||||
const dt = new Date(d)
|
||||
return dt.toLocaleString('en-GB', { timeZone: 'Europe/Paris', dateStyle: 'long', timeStyle: 'short' })
|
||||
}
|
||||
|
||||
const roomLines: string[] = []
|
||||
if (room?.roomNumber) roomLines.push(`Room: ${room.roomNumber}`)
|
||||
if (room?.checkInAt) roomLines.push(`Check-in: ${fmtDateShort(room.checkInAt)} (Paris time)`)
|
||||
if (room?.checkOutAt) roomLines.push(`Check-out: ${fmtDateShort(room.checkOutAt)} (Paris time)`)
|
||||
|
||||
const roomHtml =
|
||||
roomLines.length > 0
|
||||
? `<ul style="margin:8px 0 0;padding-left:20px;color:${BRAND.textDark};font-size:14px;">` +
|
||||
roomLines.map((l) => `<li style="margin:4px 0;">${escapeHtml(l)}</li>`).join('') +
|
||||
`</ul>`
|
||||
: ''
|
||||
|
||||
const hotelHtml = hotel
|
||||
? `<h3 style="margin:20px 0 8px;color:#0f172a;font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:1px;">Hotel</h3>` +
|
||||
infoBox(
|
||||
`<strong>${escapeHtml(hotel.name)}</strong>` +
|
||||
(hotel.address ? `<br>${escapeHtml(hotel.address)}` : '') +
|
||||
(hotel.link ? `<br><a href="${hotel.link}" style="color:${BRAND.darkBlue};">View hotel</a>` : ''),
|
||||
(hotel.link ? `<br><a href="${hotel.link}" style="color:${BRAND.darkBlue};">View hotel</a>` : '') +
|
||||
roomHtml,
|
||||
'info',
|
||||
)
|
||||
: ''
|
||||
|
||||
const roomTextLines = roomLines.length > 0 ? '\n' + roomLines.map((l) => ` ${l}`).join('\n') : ''
|
||||
const hotelText = hotel
|
||||
? ['\nHotel:', ` ${hotel.name}`, ...(hotel.address ? [` ${hotel.address}`] : []), ...(hotel.link ? [` ${hotel.link}`] : [])].join('\n')
|
||||
? ['\nHotel:', ` ${hotel.name}`, ...(hotel.address ? [` ${hotel.address}`] : []), ...(hotel.link ? [` ${hotel.link}`] : []), roomTextLines].join('\n')
|
||||
: ''
|
||||
|
||||
const content = `
|
||||
@@ -2679,6 +2704,13 @@ export const NOTIFICATION_EMAIL_TEMPLATES: Record<string, TemplateGenerator> = {
|
||||
departureAirport: ctx.metadata?.departureAirport as string | undefined,
|
||||
},
|
||||
ctx.metadata?.hotel as { name: string; address?: string; link?: string } | undefined,
|
||||
ctx.metadata?.roomNumber !== undefined || ctx.metadata?.checkInAt !== undefined || ctx.metadata?.checkOutAt !== undefined
|
||||
? {
|
||||
roomNumber: ctx.metadata?.roomNumber as string | null | undefined,
|
||||
checkInAt: ctx.metadata?.checkInAt as string | null | undefined,
|
||||
checkOutAt: ctx.metadata?.checkOutAt as string | null | undefined,
|
||||
}
|
||||
: undefined,
|
||||
),
|
||||
VISA_STATUS_UPDATE: (ctx) =>
|
||||
getVisaStatusTemplate(
|
||||
|
||||
Reference in New Issue
Block a user