From e4a9b71bfe143a49347a58ba2631c0108217e33c Mon Sep 17 00:00:00 2001 From: "diana.dolgolyova" Date: Thu, 19 Mar 2026 16:32:16 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20upgrade=20reminders=20tab=20=E2=80=94?= =?UTF-8?q?=20group=20by=20event,=20status=20tags,=20amber=20"=D0=9D=D0=B5?= =?UTF-8?q?=D1=82=20=D0=BE=D1=82=D0=B2=D0=B5=D1=82=D0=B0"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Group reminders by event within each day (e.g. "Master class · 16:00") - Stats (придёт/не придёт/нет ответа/не спрошены) shown per event, not per day - People separated by status with colored tag labels for easy scanning - "Нет ответа" now amber when active (was neutral gray, confused with unselected) - Cancelled people faded (opacity-50) Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/admin/bookings/page.tsx | 187 ++++++++++++++++++-------------- 1 file changed, 106 insertions(+), 81 deletions(-) diff --git a/src/app/admin/bookings/page.tsx b/src/app/admin/bookings/page.tsx index 4102ada..bf0d8f9 100644 --- a/src/app/admin/bookings/page.tsx +++ b/src/app/admin/bookings/page.tsx @@ -340,7 +340,7 @@ interface ReminderItem { type ReminderStatus = "pending" | "coming" | "cancelled"; const STATUS_CONFIG: Record = { - pending: { label: "Нет ответа", icon: Clock, color: "text-neutral-400", bg: "bg-neutral-500/10", border: "border-neutral-500/20" }, + pending: { label: "Нет ответа", icon: Clock, color: "text-amber-400", bg: "bg-amber-500/10", border: "border-amber-500/20" }, coming: { label: "Придёт", icon: CheckCircle2, color: "text-emerald-400", bg: "bg-emerald-500/10", border: "border-emerald-500/20" }, cancelled: { label: "Не придёт", icon: XCircle, color: "text-red-400", bg: "bg-red-500/10", border: "border-red-500/20" }, }; @@ -399,15 +399,89 @@ function RemindersTab() { ); } + // Group items by event within each day + function groupByEvent(dayItems: ReminderItem[]) { + const map: Record = {}; + for (const item of dayItems) { + const key = `${item.type}|${item.eventLabel}`; + if (!map[key]) map[key] = { type: item.type, label: item.eventLabel, items: [] }; + map[key].items.push(item); + } + return Object.values(map); + } + + const STATUS_SECTIONS = [ + { key: "not-asked", label: "Не спрошены", color: "text-gold", bg: "bg-gold/10", match: (i: ReminderItem) => !i.reminderStatus }, + { key: "pending", label: "Нет ответа", color: "text-amber-400", bg: "bg-amber-500/10", match: (i: ReminderItem) => i.reminderStatus === "pending" }, + { key: "coming", label: "Придёт", color: "text-emerald-400", bg: "bg-emerald-500/10", match: (i: ReminderItem) => i.reminderStatus === "coming" }, + { key: "cancelled", label: "Не придёт", color: "text-red-400", bg: "bg-red-500/10", match: (i: ReminderItem) => i.reminderStatus === "cancelled" }, + ]; + + function renderPerson(item: ReminderItem) { + const currentStatus = item.reminderStatus as ReminderStatus | undefined; + return ( +
+
+ {item.name} + {item.phone && ( + + {item.phone} + + )} + {item.instagram && ( + + {item.instagram} + + )} + {item.telegram && ( + + {item.telegram} + + )} +
+ {(["coming", "pending", "cancelled"] as ReminderStatus[]).map((st) => { + const conf = STATUS_CONFIG[st]; + const Icon = conf.icon; + const active = currentStatus === st; + return ( + + ); + })} +
+
+
+ ); + } + return (
{[ { label: "Сегодня", date: today, items: todayItems }, { label: "Завтра", date: tomorrow, items: tomorrowItems }, ] - .filter((group) => group.items.length > 0) + .filter((g) => g.items.length > 0) .map((group) => { - const stats = countByStatus(group.items); + const eventGroups = groupByEvent(group.items); return (
@@ -415,90 +489,41 @@ function RemindersTab() { {new Date(group.date + "T12:00").toLocaleDateString("ru-RU", { weekday: "long", day: "numeric", month: "long" })} -
- {stats.coming > 0 && {stats.coming} придёт} - {stats.cancelled > 0 && {stats.cancelled} не придёт} - {stats.pending > 0 && {stats.pending} нет ответа} - {stats.notAsked > 0 && {stats.notAsked} не спрошены} -
-
- {group.items.map((item) => { - const typeConf = TYPE_CONFIG[item.type]; +
+ {eventGroups.map((eg) => { + const typeConf = TYPE_CONFIG[eg.type]; const TypeIcon = typeConf.icon; - const currentStatus = item.reminderStatus as ReminderStatus | undefined; - + const egStats = countByStatus(eg.items); return ( -
-
- {item.name} - {item.phone && ( - - {item.phone} - - )} - {item.instagram && ( - - {item.instagram} - - )} - {item.telegram && ( - - {item.telegram} - - )} -
- -
- - - {item.eventLabel} - - -
- {(["coming", "pending", "cancelled"] as ReminderStatus[]).map((st) => { - const conf = STATUS_CONFIG[st]; - const Icon = conf.icon; - const active = currentStatus === st; - return ( - - ); - })} +
+
+ + {eg.label} + {eg.items.length} чел. +
+ {egStats.coming > 0 && {egStats.coming} придёт} + {egStats.cancelled > 0 && {egStats.cancelled} не придёт} + {egStats.pending > 0 && {egStats.pending} нет ответа} + {egStats.notAsked > 0 && {egStats.notAsked} не спрошены}
+
+ {STATUS_SECTIONS + .map((sec) => ({ ...sec, items: eg.items.filter(sec.match) })) + .filter((sec) => sec.items.length > 0) + .map((sec) => ( +
+ + {sec.label} · {sec.items.length} + +
+ {sec.items.map(renderPerson)} +
+
+ ))} +
); })}