diff --git a/backend/src/routes/deposit.ts b/backend/src/routes/deposit.ts index f999378..c698c47 100644 --- a/backend/src/routes/deposit.ts +++ b/backend/src/routes/deposit.ts @@ -263,31 +263,101 @@ const deposit: FastifyPluginAsync = async (fastify) => { // Send email to guest if email is available const { rows: bRows } = await db.query( - 'SELECT guest_name, guest_email FROM bookings WHERE id = $1', + `SELECT b.guest_name, b.guest_email, h.name AS hotel_name + FROM bookings b + JOIN hotels h ON h.id = b.hotel_id + WHERE b.id = $1`, [bookingId], ) if (bRows[0]?.guest_email) { const guest = bRows[0] - let bodyLines: string[] + const hotelName: string = guest.hotel_name ?? 'HotelSync' + const fmt = (n: number) => n.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' ₽' + const refund = Number(dep.amount) - capturedAmount + + let html: string if (capturedAmount === 0) { - bodyLines = ['Депозит был полностью возвращён. Средства поступят на карту в течение нескольких рабочих дней.'] + html = ` +

Уважаемый(ая) ${guest.guest_name},

+

Страховой депозит был полностью возвращён.

+

Средства поступят на вашу карту в течение нескольких рабочих дней.

` } else { - bodyLines = [`Из страхового депозита удержана сумма ${capturedAmount.toFixed(2)} ₽.`] - if (items && items.length > 0) { - bodyLines.push('', 'Позиции удержания:') - for (const item of items) { - bodyLines.push(` • ${item.name} — ${Number(item.amount).toFixed(2)} ₽`) - } - } - if (reason) bodyLines.push('', `Комментарий: ${reason}`) - const refund = Number(dep.amount) - capturedAmount - if (refund > 0) bodyLines.push('', `Остаток депозита ${refund.toFixed(2)} ₽ возвращён.`) + const itemsHtml = items && items.length > 0 + ? ` + + + + + + + + ${items.map((it, i) => ` + + + + `).join('')} + + + + + + + ${refund > 0 ? `` : ''} + +
ПозицияСумма
${it.name}${fmt(Number(it.amount))}
Итого удержано${fmt(capturedAmount)}
Возврат остатка${fmt(refund)}
` + : `

Удержана сумма: ${fmt(capturedAmount)}

${refund > 0 ? `

Остаток ${fmt(refund)} возвращён.

` : ''}` + + html = ` +

Уважаемый(ая) ${guest.guest_name},

+

Из страхового депозита удержана сумма за следующие позиции:

+${itemsHtml} +${reason ? `

Комментарий: ${reason}

` : ''} +${refund > 0 && (!items || items.length === 0) ? `

Остаток депозита ${fmt(refund)} возвращён.

` : ''}` } + + const emailHtml = ` + + + + +
+ + + + +
+

${hotelName}

+

Информация о страховом депозите

+
+ ${html} +

Спасибо за проживание.

+
+

© 2026 ${hotelName} · Powered by HotelSync

+
+
+` + + const textFallback = capturedAmount === 0 + ? `Уважаемый(ая) ${guest.guest_name},\n\nСтраховой депозит полностью возвращён.\n\nСпасибо за проживание.\n\n© 2026 ${hotelName}` + : [ + `Уважаемый(ая) ${guest.guest_name},`, + '', + `Из страхового депозита удержана сумма ${fmt(capturedAmount)}.`, + ...(items && items.length > 0 ? ['', 'Позиции удержания:', ...items.map(it => ` • ${it.name} — ${fmt(Number(it.amount))}`)] : []), + ...(reason ? ['', `Комментарий: ${reason}`] : []), + ...(refund > 0 ? ['', `Остаток депозита ${fmt(refund)} возвращён.`] : []), + '', + 'Спасибо за проживание.', + '', + `© 2026 ${hotelName}`, + ].join('\n') + transporter.sendMail({ - from: `"HotelSync" <${process.env.SMTP_USER ?? 'noreply@hotelsync.ru'}>`, + from: `"${hotelName}" <${process.env.SMTP_USER ?? 'noreply@hotelsync.ru'}>`, to: guest.guest_email, - subject: 'Информация о депозите — HotelSync', - text: [`Уважаемый(ая) ${guest.guest_name},`, '', ...bodyLines, '', 'Спасибо за проживание.', '', '© 2026 HotelSync'].join('\n'), + subject: `Информация о депозите — ${hotelName}`, + text: textFallback, + html: emailHtml, }).catch(() => {}) await db.query( diff --git a/src/components/bookings/BookingDetailPanel.tsx b/src/components/bookings/BookingDetailPanel.tsx index d2c0fcd..ad86f74 100644 --- a/src/components/bookings/BookingDetailPanel.tsx +++ b/src/components/bookings/BookingDetailPanel.tsx @@ -317,19 +317,42 @@ function DepositWidget({ slug, bookingId }: { slug: string; bookingId: string })

Возврат / Удержание депозита

+ {/* Minibar items breakdown */} + {minibarItems.length > 0 && ( +
+
+ Минибар — {formatCurrency(minibarTotal)} + +
+
+ {minibarItems.map((it, i) => ( +
+ + {it.itemName} × {it.quantity} ({formatCurrency(it.pricePerUnit)} / шт.) + + {formatCurrency(it.lineTotal)} + +
+ ))} +
+
+ )} + {/* Quick presets from settings */} - {(presets.length > 0 || minibarTotal > 0) && ( + {(presets.length > 0 || minibarTotal === 0) && (

Добавить позицию:

- {minibarTotal > 0 && ( - - )} {presets.map(p => (
)} + {presets.length === 0 && minibarTotal > 0 && ( + + )} {/* Added items */} {releaseItems.length > 0 && ( @@ -383,15 +414,6 @@ function DepositWidget({ slug, bookingId }: { slug: string; bookingId: string })

✓ Полный возврат депозита

)} - {/* Quick add if no presets */} - {presets.length === 0 && minibarTotal === 0 && ( - - )}