feat: upgrade reminders tab — group by event, status tags, amber "Нет ответа"
- 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) <noreply@anthropic.com>
This commit is contained in:
@@ -340,7 +340,7 @@ interface ReminderItem {
|
||||
type ReminderStatus = "pending" | "coming" | "cancelled";
|
||||
|
||||
const STATUS_CONFIG: Record<ReminderStatus, { label: string; icon: typeof CheckCircle2; color: string; bg: string; border: string }> = {
|
||||
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,47 +399,35 @@ function RemindersTab() {
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{[
|
||||
{ label: "Сегодня", date: today, items: todayItems },
|
||||
{ label: "Завтра", date: tomorrow, items: tomorrowItems },
|
||||
]
|
||||
.filter((group) => group.items.length > 0)
|
||||
.map((group) => {
|
||||
const stats = countByStatus(group.items);
|
||||
return (
|
||||
<div key={group.date}>
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<h3 className="text-sm font-bold text-white">{group.label}</h3>
|
||||
<span className="text-[10px] text-neutral-500">
|
||||
{new Date(group.date + "T12:00").toLocaleDateString("ru-RU", { weekday: "long", day: "numeric", month: "long" })}
|
||||
</span>
|
||||
<div className="flex gap-2 ml-auto text-[10px]">
|
||||
{stats.coming > 0 && <span className="text-emerald-400">{stats.coming} придёт</span>}
|
||||
{stats.cancelled > 0 && <span className="text-red-400">{stats.cancelled} не придёт</span>}
|
||||
{stats.pending > 0 && <span className="text-neutral-400">{stats.pending} нет ответа</span>}
|
||||
{stats.notAsked > 0 && <span className="text-gold">{stats.notAsked} не спрошены</span>}
|
||||
</div>
|
||||
</div>
|
||||
// Group items by event within each day
|
||||
function groupByEvent(dayItems: ReminderItem[]) {
|
||||
const map: Record<string, { type: ReminderItem["type"]; label: string; items: ReminderItem[] }> = {};
|
||||
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);
|
||||
}
|
||||
|
||||
<div className="space-y-1.5">
|
||||
{group.items.map((item) => {
|
||||
const typeConf = TYPE_CONFIG[item.type];
|
||||
const TypeIcon = typeConf.icon;
|
||||
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 (
|
||||
<div
|
||||
key={`${item.table}-${item.id}`}
|
||||
className={`rounded-xl border p-3 transition-colors ${
|
||||
!currentStatus
|
||||
? "border-gold/20 bg-gold/[0.03]"
|
||||
: currentStatus === "coming"
|
||||
? "border-emerald-500/15 bg-emerald-500/[0.02]"
|
||||
: currentStatus === "cancelled"
|
||||
? "border-red-500/15 bg-red-500/[0.02] opacity-60"
|
||||
: "border-white/10 bg-neutral-900"
|
||||
className={`rounded-lg border p-3 transition-colors ${
|
||||
!currentStatus ? "border-gold/20 bg-gold/[0.03]"
|
||||
: currentStatus === "coming" ? "border-emerald-500/15 bg-emerald-500/[0.02]"
|
||||
: currentStatus === "cancelled" ? "border-red-500/15 bg-red-500/[0.02] opacity-50"
|
||||
: currentStatus === "pending" ? "border-amber-500/15 bg-amber-500/[0.02]"
|
||||
: "border-white/5 bg-neutral-800/30"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-wrap text-sm">
|
||||
@@ -450,33 +438,15 @@ function RemindersTab() {
|
||||
</a>
|
||||
)}
|
||||
{item.instagram && (
|
||||
<a
|
||||
href={`https://ig.me/m/${item.instagram.replace(/^@/, "")}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 text-pink-400 hover:text-pink-300 text-xs"
|
||||
>
|
||||
<a href={`https://ig.me/m/${item.instagram.replace(/^@/, "")}`} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 text-pink-400 hover:text-pink-300 text-xs">
|
||||
<Instagram size={10} />{item.instagram}
|
||||
</a>
|
||||
)}
|
||||
{item.telegram && (
|
||||
<a
|
||||
href={`https://t.me/${item.telegram.replace(/^@/, "")}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 text-blue-400 hover:text-blue-300 text-xs"
|
||||
>
|
||||
<a href={`https://t.me/${item.telegram.replace(/^@/, "")}`} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 text-blue-400 hover:text-blue-300 text-xs">
|
||||
<Send size={10} />{item.telegram}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 mt-2 flex-wrap">
|
||||
<span className={`inline-flex items-center gap-1 text-[10px] ${typeConf.color}`}>
|
||||
<TypeIcon size={10} />
|
||||
{item.eventLabel}
|
||||
</span>
|
||||
|
||||
<div className="flex gap-1 ml-auto">
|
||||
{(["coming", "pending", "cancelled"] as ReminderStatus[]).map((st) => {
|
||||
const conf = STATUS_CONFIG[st];
|
||||
@@ -501,6 +471,61 @@ function RemindersTab() {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{[
|
||||
{ label: "Сегодня", date: today, items: todayItems },
|
||||
{ label: "Завтра", date: tomorrow, items: tomorrowItems },
|
||||
]
|
||||
.filter((g) => g.items.length > 0)
|
||||
.map((group) => {
|
||||
const eventGroups = groupByEvent(group.items);
|
||||
return (
|
||||
<div key={group.date}>
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<h3 className="text-sm font-bold text-white">{group.label}</h3>
|
||||
<span className="text-[10px] text-neutral-500">
|
||||
{new Date(group.date + "T12:00").toLocaleDateString("ru-RU", { weekday: "long", day: "numeric", month: "long" })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{eventGroups.map((eg) => {
|
||||
const typeConf = TYPE_CONFIG[eg.type];
|
||||
const TypeIcon = typeConf.icon;
|
||||
const egStats = countByStatus(eg.items);
|
||||
return (
|
||||
<div key={eg.label} className="rounded-xl border border-white/10 overflow-hidden">
|
||||
<div className="flex items-center gap-2 px-4 py-2.5 bg-neutral-900">
|
||||
<TypeIcon size={13} className={typeConf.color} />
|
||||
<span className="text-sm font-medium text-white">{eg.label}</span>
|
||||
<span className="text-[10px] text-neutral-500 bg-neutral-800 rounded-full px-2 py-0.5">{eg.items.length} чел.</span>
|
||||
<div className="flex gap-2 ml-auto text-[10px]">
|
||||
{egStats.coming > 0 && <span className="text-emerald-400">{egStats.coming} придёт</span>}
|
||||
{egStats.cancelled > 0 && <span className="text-red-400">{egStats.cancelled} не придёт</span>}
|
||||
{egStats.pending > 0 && <span className="text-amber-400">{egStats.pending} нет ответа</span>}
|
||||
{egStats.notAsked > 0 && <span className="text-gold">{egStats.notAsked} не спрошены</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-4 pb-3 pt-1">
|
||||
{STATUS_SECTIONS
|
||||
.map((sec) => ({ ...sec, items: eg.items.filter(sec.match) }))
|
||||
.filter((sec) => sec.items.length > 0)
|
||||
.map((sec) => (
|
||||
<div key={sec.key} className="mt-2 first:mt-0">
|
||||
<span className={`text-[10px] font-medium ${sec.color} ${sec.bg} rounded-full px-2 py-0.5`}>
|
||||
{sec.label} · {sec.items.length}
|
||||
</span>
|
||||
<div className="mt-1.5 space-y-1.5">
|
||||
{sec.items.map(renderPerson)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user