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
102 lines
4.0 KiB
TypeScript
102 lines
4.0 KiB
TypeScript
"use client";
|
|
|
|
import { useRouter, usePathname } from "next/navigation";
|
|
import { CopyButton } from "@/components/copy-button";
|
|
|
|
function formatMonthLabel(monthKey: string): string {
|
|
const [y, m] = monthKey.split("-").map(Number);
|
|
const labels = ["янв", "фев", "мар", "апр", "май", "июн", "июл", "авг", "сен", "окт", "ноя", "дек"];
|
|
return `${labels[m - 1]} ${y}`;
|
|
}
|
|
|
|
interface MonthlyMoneyStats {
|
|
totalCost: number;
|
|
totalPrepayment: number;
|
|
totalDiscount: number;
|
|
totalOutstanding: number;
|
|
}
|
|
|
|
function fmtMoney(n: number): string {
|
|
return n.toLocaleString("ru-RU");
|
|
}
|
|
|
|
export function StatisticsView({
|
|
monthKey,
|
|
stats,
|
|
money,
|
|
}: {
|
|
monthKey: string;
|
|
stats: { name: string; count: number }[];
|
|
money: MonthlyMoneyStats;
|
|
}) {
|
|
const router = useRouter();
|
|
const pathname = usePathname();
|
|
const total = stats.reduce((s, r) => s + r.count, 0);
|
|
const max = Math.max(1, ...stats.map((r) => r.count));
|
|
|
|
const summaryText = [
|
|
`Статистика по бронированиям на ${formatMonthLabel(monthKey)}:`,
|
|
"",
|
|
...stats.map((r) => `${r.name}: ${r.count}`),
|
|
"",
|
|
`Сумма бронирований: ${fmtMoney(money.totalCost)} ₽`,
|
|
`Собрано предоплат: ${fmtMoney(money.totalPrepayment)} ₽`,
|
|
`Остаток к оплате: ${fmtMoney(money.totalOutstanding)} ₽`,
|
|
money.totalDiscount > 0 ? `Скидок дано: ${fmtMoney(money.totalDiscount)} ₽` : null,
|
|
]
|
|
.filter((l) => l !== null)
|
|
.join("\n");
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4">
|
|
<div className="card flex flex-wrap items-center justify-between gap-3 p-3">
|
|
<input
|
|
type="month"
|
|
value={monthKey}
|
|
onChange={(e) => router.push(`${pathname}?month=${e.target.value}`)}
|
|
className="rounded-md border border-border bg-surface px-3 py-1.5 text-sm outline-none focus:border-accent"
|
|
/>
|
|
<span className="text-sm text-text-muted">Всего бронирований: {total}</span>
|
|
{stats.length > 0 && <CopyButton text={summaryText} label="Скопировать статистику" size="lg" />}
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
|
<div className="card p-3">
|
|
<p className="mb-1 text-xs font-medium text-text-muted">Сумма бронирований</p>
|
|
<p className="font-mono text-base font-semibold">{fmtMoney(money.totalCost)} ₽</p>
|
|
</div>
|
|
<div className="card p-3">
|
|
<p className="mb-1 text-xs font-medium text-text-muted">Собрано предоплат</p>
|
|
<p className="font-mono text-base font-semibold text-success">{fmtMoney(money.totalPrepayment)} ₽</p>
|
|
</div>
|
|
<div className="card p-3">
|
|
<p className="mb-1 text-xs font-medium text-text-muted">Остаток к оплате</p>
|
|
<p className="font-mono text-base font-semibold text-danger">{fmtMoney(money.totalOutstanding)} ₽</p>
|
|
</div>
|
|
<div className="card p-3">
|
|
<p className="mb-1 text-xs font-medium text-text-muted">Скидок дано</p>
|
|
<p className="font-mono text-base font-semibold">{fmtMoney(money.totalDiscount)} ₽</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card p-4">
|
|
{stats.length === 0 ? (
|
|
<p className="text-sm text-text-muted">За этот месяц бронирований нет.</p>
|
|
) : (
|
|
<div className="flex flex-col gap-3">
|
|
{stats.map((r) => (
|
|
<div key={r.name} className="flex items-center gap-3">
|
|
<span className="w-40 shrink-0 truncate text-sm">{r.name}</span>
|
|
<div className="h-2.5 flex-1 overflow-hidden rounded-full bg-surface-hover">
|
|
<div className="h-full rounded-full bg-accent" style={{ width: `${(r.count / max) * 100}%` }} />
|
|
</div>
|
|
<span className="w-8 shrink-0 text-right text-sm font-medium">{r.count}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|