Rewrite guest confirmation text; add discount and multi-day checkout
Copy button text now matches the exact guest-facing template requested — "Ваше бронирование подтверждено!", Гость/Номер для связи/Объект бронирования/Количество гостей/Дополнительные пожелания/Скидка — dropping the internal-only fields (source, who booked it) since this text gets pasted straight to the client, not kept for internal records (those still show on the on-screen summary card). New checkOutDate column (nullable — null means same-day, the common case) lets a booking span multiple days; the Google Calendar link and copy text both show the date range when set. New discount column (same shape as prepayment) feeds the new "Скидка" line. Statistics page gains a money row for the selected month: total booked, prepayments collected, amount still outstanding, and total discount given. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gh2UXUQUVBWroWEnn1FLFG
This commit is contained in:
1 parent
4f206c2ddc
commit
006d108f54
15 files changed
+600
-35
No files matched your search
@@ -22,8 +22,8 @@ function toGoogleDateParam(d: Date): string {
|
||||
*/
|
||||
export function buildGoogleCalendarUrl(booking: BookingDTO): string {
|
||||
const start = moscowLocalToUtc(booking.date, booking.checkIn);
|
||||
let end = moscowLocalToUtc(booking.date, booking.checkOut);
|
||||
if (end <= start) end = new Date(end.getTime() + 24 * 3600_000); // overnight check-out
|
||||
let end = moscowLocalToUtc(booking.checkOutDate ?? booking.date, booking.checkOut);
|
||||
if (end <= start) end = new Date(end.getTime() + 24 * 3600_000); // overnight check-out, no explicit checkout date
|
||||
|
||||
const objectNames = booking.objects.map((o) => o.name).join(", ") || "—";
|
||||
const details = formatBookingSummary({
|
||||
@@ -32,11 +32,13 @@ export function buildGoogleCalendarUrl(booking: BookingDTO): string {
|
||||
phone: booking.phone,
|
||||
objectNames: booking.objects.map((o) => o.name),
|
||||
date: booking.date,
|
||||
checkOutDate: booking.checkOutDate,
|
||||
checkIn: booking.checkIn,
|
||||
checkOut: booking.checkOut,
|
||||
guestCount: booking.guestCount,
|
||||
cost: booking.cost,
|
||||
prepayment: booking.prepayment,
|
||||
discount: booking.discount,
|
||||
comment: booking.comment,
|
||||
source: booking.source,
|
||||
bookedByName: booking.bookedByName,
|
||||
|
||||
+16
-12
@@ -4,11 +4,13 @@ export interface BookingSummaryInput {
|
||||
phone: string;
|
||||
objectNames: string[];
|
||||
date: Date;
|
||||
checkOutDate: Date | null;
|
||||
checkIn: string;
|
||||
checkOut: string;
|
||||
guestCount: number;
|
||||
cost: number;
|
||||
prepayment: number;
|
||||
discount: number;
|
||||
comment: string | null;
|
||||
source: string | null;
|
||||
bookedByName: string;
|
||||
@@ -21,24 +23,26 @@ function formatDate(d: Date): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Plain-text booking summary — deliberately mirrors the old Telegram bot's
|
||||
* "Готово! Вот что я забронировал:" message format so staff can paste it
|
||||
* anywhere they're used to pasting the bot's output.
|
||||
* Guest-facing confirmation text for the "Скопировать бронирование" button
|
||||
* — meant to be pasted straight to the client (WhatsApp/Telegram/SMS), so
|
||||
* it deliberately excludes internal-only fields (source, who booked it).
|
||||
* Field set and order match exactly what was asked for; a multi-day stay's
|
||||
* date line shows both days ("дд.мм.гггг - дд.мм.гггг").
|
||||
*/
|
||||
export function formatBookingSummary(b: BookingSummaryInput): string {
|
||||
const dateLine = b.checkOutDate ? `${formatDate(b.date)} - ${formatDate(b.checkOutDate)}` : formatDate(b.date);
|
||||
const lines = [
|
||||
`Номер бронирования: ${b.bookingNumber}`,
|
||||
`Контактное лицо: ${b.contactName}`,
|
||||
b.phone,
|
||||
`Объект: ${b.objectNames.join("/") || "—"}`,
|
||||
`Дата: ${formatDate(b.date)}`,
|
||||
`Ваше бронирование подтверждено! Номер бронирования: ${b.bookingNumber}`,
|
||||
`Гость: ${b.contactName}`,
|
||||
`Номер для связи: ${b.phone}`,
|
||||
`Объект бронирования: ${b.objectNames.join("/") || "—"}`,
|
||||
`Дата: ${dateLine}`,
|
||||
`Время заезда/выезда: ${b.checkIn} - ${b.checkOut}`,
|
||||
`Количество людей: ${b.guestCount}`,
|
||||
`Количество гостей: ${b.guestCount}`,
|
||||
`Стоимость бронирования: ${b.cost}`,
|
||||
`Предоплата: ${b.prepayment}`,
|
||||
`Комментарий: ${b.comment?.trim() || "-"}`,
|
||||
`Откуда пришли: ${b.source?.trim() || "-"}`,
|
||||
`Кто забронировал: ${b.bookedByName}`,
|
||||
`Дополнительные пожелания: ${b.comment?.trim() || "-"}`,
|
||||
`Скидка: ${b.discount}`,
|
||||
];
|
||||
return lines.join("\n");
|
||||
}
|
||||
@@ -42,11 +42,13 @@ export interface BookingDTO {
|
||||
contactName: string;
|
||||
phone: string;
|
||||
date: Date;
|
||||
checkOutDate: Date | null;
|
||||
checkIn: string;
|
||||
checkOut: string;
|
||||
guestCount: number;
|
||||
cost: number;
|
||||
prepayment: number;
|
||||
discount: number;
|
||||
comment: string | null;
|
||||
source: string | null;
|
||||
status: "confirmed" | "cancelled";
|
||||
@@ -71,11 +73,13 @@ async function toBookingDTO(row: typeof bookings.$inferSelect): Promise<BookingD
|
||||
contactName: row.contactName,
|
||||
phone: row.phone,
|
||||
date: row.date,
|
||||
checkOutDate: row.checkOutDate,
|
||||
checkIn: row.checkIn,
|
||||
checkOut: row.checkOut,
|
||||
guestCount: row.guestCount,
|
||||
cost: row.cost,
|
||||
prepayment: row.prepayment,
|
||||
discount: row.discount,
|
||||
comment: row.comment,
|
||||
source: row.source,
|
||||
status: row.status,
|
||||
@@ -90,11 +94,13 @@ export interface CreateBookingInput {
|
||||
phone: string;
|
||||
objectIds: string[];
|
||||
date: Date;
|
||||
checkOutDate?: Date | null;
|
||||
checkIn: string;
|
||||
checkOut: string;
|
||||
guestCount: number;
|
||||
cost: number;
|
||||
prepayment: number;
|
||||
discount?: number;
|
||||
comment?: string | null;
|
||||
source?: string | null;
|
||||
bookedByUserId: string;
|
||||
@@ -111,11 +117,13 @@ export async function createBooking(input: CreateBookingInput): Promise<BookingD
|
||||
contactName: input.contactName,
|
||||
phone: input.phone,
|
||||
date: input.date,
|
||||
checkOutDate: input.checkOutDate ?? null,
|
||||
checkIn: input.checkIn,
|
||||
checkOut: input.checkOut,
|
||||
guestCount: input.guestCount,
|
||||
cost: input.cost,
|
||||
prepayment: input.prepayment,
|
||||
discount: input.discount ?? 0,
|
||||
comment: input.comment ?? null,
|
||||
source: input.source ?? null,
|
||||
bookedByUserId: input.bookedByUserId,
|
||||
@@ -134,11 +142,13 @@ export interface UpdateBookingInput {
|
||||
phone?: string;
|
||||
objectIds?: string[];
|
||||
date?: Date;
|
||||
checkOutDate?: Date | null;
|
||||
checkIn?: string;
|
||||
checkOut?: string;
|
||||
guestCount?: number;
|
||||
cost?: number;
|
||||
prepayment?: number;
|
||||
discount?: number;
|
||||
comment?: string | null;
|
||||
source?: string | null;
|
||||
status?: "confirmed" | "cancelled";
|
||||
@@ -216,6 +226,36 @@ export async function getMonthlyObjectStats(monthKey: string): Promise<{ name: s
|
||||
return [...counts.entries()].map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count);
|
||||
}
|
||||
|
||||
export interface MonthlyMoneyStats {
|
||||
totalCost: number;
|
||||
totalPrepayment: number;
|
||||
totalDiscount: number;
|
||||
/** cost already net of discount at entry time — this is just prepayment collected vs. still owed on confirmed bookings. */
|
||||
totalOutstanding: number;
|
||||
}
|
||||
|
||||
/** Money totals for the month — confirmed bookings only, same rule as getMonthlyObjectStats. */
|
||||
export async function getMonthlyMoneyStats(monthKey: string): Promise<MonthlyMoneyStats> {
|
||||
const [y, m] = monthKey.split("-").map(Number);
|
||||
const from = new Date(Date.UTC(y, m - 1, 1));
|
||||
const to = new Date(Date.UTC(y, m, 1));
|
||||
|
||||
const rows = await db
|
||||
.select({ cost: bookings.cost, prepayment: bookings.prepayment, discount: bookings.discount })
|
||||
.from(bookings)
|
||||
.where(and(gte(bookings.date, from), lt(bookings.date, to), eq(bookings.status, "confirmed")));
|
||||
|
||||
return rows.reduce(
|
||||
(acc, r) => ({
|
||||
totalCost: acc.totalCost + r.cost,
|
||||
totalPrepayment: acc.totalPrepayment + r.prepayment,
|
||||
totalDiscount: acc.totalDiscount + r.discount,
|
||||
totalOutstanding: acc.totalOutstanding + (r.cost - r.prepayment),
|
||||
}),
|
||||
{ totalCost: 0, totalPrepayment: 0, totalDiscount: 0, totalOutstanding: 0 },
|
||||
);
|
||||
}
|
||||
|
||||
export async function listBookableObjectsByIds(ids: string[]) {
|
||||
if (ids.length === 0) return [];
|
||||
return db.select().from(bookableObjects).where(inArray(bookableObjects.id, ids));
|
||||
|
||||
Reference in new issue
Block a user