From 67d8f6330ca91a8f481ee44da60448fa0ca4aeef Mon Sep 17 00:00:00 2001 From: "diana.dolgolyova" Date: Tue, 24 Mar 2026 18:56:39 +0300 Subject: [PATCH] refactor: dashboard cards with clickable status counts + tab bar restored MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Dashboard cards show all 4 statuses inline: new (gold), contacted (blue), confirmed (green), declined (red) — big numbers with consistent status colors - Each number+label is clickable to filter the tab by that status - Tab bar restored below dashboard - Removed filter chips from SearchBar (dashboard handles filtering) - Open Day card uses cyan border (distinct from blue contacted status) --- src/app/admin/bookings/SearchBar.tsx | 60 +++------- src/app/admin/bookings/page.tsx | 167 ++++++++++++++++----------- 2 files changed, 113 insertions(+), 114 deletions(-) diff --git a/src/app/admin/bookings/SearchBar.tsx b/src/app/admin/bookings/SearchBar.tsx index 8730536..ad72a12 100644 --- a/src/app/admin/bookings/SearchBar.tsx +++ b/src/app/admin/bookings/SearchBar.tsx @@ -1,18 +1,14 @@ "use client"; import { useState, useRef } from "react"; -import { Search, X, Filter } from "lucide-react"; +import { Search, X } from "lucide-react"; import { adminFetch } from "@/lib/csrf"; -import { type BookingFilter, type SearchResult, BOOKING_STATUSES } from "./types"; +import type { SearchResult } from "./types"; export function SearchBar({ - filter, - onFilterChange, onResults, onClear, }: { - filter: BookingFilter; - onFilterChange: (f: BookingFilter) => void; onResults: (results: SearchResult[]) => void; onClear: () => void; }) { @@ -40,45 +36,19 @@ export function SearchBar({ } return ( -
-
- - handleChange(e.target.value)} - placeholder="Поиск по имени или телефону..." - className="w-full rounded-lg border border-white/[0.08] bg-white/[0.04] py-2 pl-9 pr-8 text-sm text-white placeholder-neutral-500 outline-none focus:border-gold/40" - /> - {query && ( - - )} -
- {( -
- - - {BOOKING_STATUSES.map((s) => ( - - ))} -
+
+ + handleChange(e.target.value)} + placeholder="Поиск по имени или телефону..." + className="w-full rounded-lg border border-white/[0.08] bg-white/[0.04] py-2 pl-9 pr-8 text-sm text-white placeholder-neutral-500 outline-none focus:border-gold/40" + /> + {query && ( + )}
); diff --git a/src/app/admin/bookings/page.tsx b/src/app/admin/bookings/page.tsx index 8ac709c..4c8aa38 100644 --- a/src/app/admin/bookings/page.tsx +++ b/src/app/admin/bookings/page.tsx @@ -2,10 +2,10 @@ import { useState, useEffect, useMemo, useCallback, useRef } from "react"; import { createPortal } from "react-dom"; -import { Phone, Instagram, Send, ChevronDown, ChevronRight, Bell, CheckCircle2, XCircle, Clock, Star, Calendar, DoorOpen, X, Plus } from "lucide-react"; +import { Phone, Instagram, Send, Bell, CheckCircle2, XCircle, Clock, Star, Calendar, DoorOpen, X, Plus } from "lucide-react"; import { adminFetch } from "@/lib/csrf"; import { MS_PER_DAY } from "@/lib/constants"; -import { type BookingStatus, type BookingFilter, type SearchResult, BOOKING_STATUSES, SHORT_DAYS, fmtDate } from "./types"; +import { type BookingStatus, type BookingFilter, type SearchResult, SHORT_DAYS, fmtDate } from "./types"; import { LoadingSpinner, ContactLinks, BookingCard, StatusBadge, StatusActions, DeleteBtn } from "./BookingComponents"; import { GenericBookingsList } from "./GenericBookingsList"; import { AddBookingModal } from "./AddBookingModal"; @@ -574,18 +574,28 @@ function RemindersTab() { // --- Dashboard Summary --- +interface TabCounts { new: number; contacted: number; confirmed: number; declined: number } + interface DashboardCounts { - classesNew: number; - classesContacted: number; - mcNew: number; - mcContacted: number; - odNew: number; - odContacted: number; + classes: TabCounts; + mc: TabCounts; + od: TabCounts; remindersToday: number; remindersTomorrow: number; } -function DashboardSummary({ onNavigate }: { onNavigate: (tab: Tab) => void }) { +function countByStatus(items: { status: string }[]): TabCounts { + const c = { new: 0, contacted: 0, confirmed: 0, declined: 0 }; + for (const i of items) if (i.status in c) c[i.status as keyof TabCounts]++; + return c; +} + + +function DashboardSummary({ statusFilter, onNavigate, onFilter }: { + statusFilter: BookingFilter; + onNavigate: (tab: Tab) => void; + onFilter: (f: BookingFilter) => void; +}) { const [counts, setCounts] = useState(null); useEffect(() => { @@ -594,12 +604,10 @@ function DashboardSummary({ onNavigate }: { onNavigate: (tab: Tab) => void }) { Promise.all([ adminFetch("/api/admin/group-bookings").then((r) => r.json()), - // Fetch MC registrations + section data to filter out archived Promise.all([ adminFetch("/api/admin/mc-registrations").then((r) => r.json()), adminFetch("/api/admin/sections/masterClasses").then((r) => r.json()), ]).then(([regs, mcData]: [{ status: string; masterClassTitle: string }[], { items?: { title: string; slots: { date: string }[] }[] }]) => { - // Build set of upcoming MC titles const upcomingTitles = new Set(); for (const mc of mcData.items || []) { const earliest = mc.slots?.reduce((min, s) => s.date < min ? s.date : min, mc.slots[0]?.date ?? ""); @@ -607,7 +615,6 @@ function DashboardSummary({ onNavigate }: { onNavigate: (tab: Tab) => void }) { } return regs.filter((r) => upcomingTitles.has(r.masterClassTitle)); }), - // Fetch Open Day — only upcoming events adminFetch("/api/admin/open-day").then((r) => r.json()).then(async (events: { id: number; date: string }[]) => { const active = events.find((e) => e.date >= today); if (!active) return []; @@ -616,54 +623,57 @@ function DashboardSummary({ onNavigate }: { onNavigate: (tab: Tab) => void }) { adminFetch("/api/admin/reminders").then((r) => r.json()).catch(() => []), ]).then(([gb, mc, od, rem]: [{ status: string }[], { status: string }[], { status: string }[], { eventDate: string }[]]) => { setCounts({ - classesNew: gb.filter((b) => b.status === "new").length, - classesContacted: gb.filter((b) => b.status === "contacted").length, - mcNew: mc.filter((b) => b.status === "new").length, - mcContacted: mc.filter((b) => b.status === "contacted").length, - odNew: od.filter((b) => b.status === "new").length, - odContacted: od.filter((b) => b.status === "contacted").length, + classes: countByStatus(gb), + mc: countByStatus(mc), + od: countByStatus(od), remindersToday: rem.filter((r) => r.eventDate === today).length, remindersTomorrow: rem.filter((r) => r.eventDate === tomorrow).length, }); - }).catch(() => {}); // Dashboard is non-critical, silent fail OK + }).catch(() => {}); }, []); if (!counts) return null; - const cards: { tab: Tab; label: string; urgent: number; urgentLabel: string; pending: number; pendingLabel: string; color: string; urgentColor: string }[] = [ - { - tab: "reminders", label: "Напоминания", - urgent: counts.remindersToday, urgentLabel: "сегодня", - pending: counts.remindersTomorrow, pendingLabel: "завтра", - color: "border-amber-500/20", urgentColor: "text-amber-400", - }, - { - tab: "classes", label: "Занятия", - urgent: counts.classesNew, urgentLabel: "новых", - pending: counts.classesContacted, pendingLabel: "в работе", - color: "border-gold/20", urgentColor: "text-gold", - }, - { - tab: "master-classes", label: "Мастер-классы", - urgent: counts.mcNew, urgentLabel: "новых", - pending: counts.mcContacted, pendingLabel: "в работе", - color: "border-purple-500/20", urgentColor: "text-purple-400", - }, - { - tab: "open-day", label: "Open Day", - urgent: counts.odNew, urgentLabel: "новых", - pending: counts.odContacted, pendingLabel: "в работе", - color: "border-blue-500/20", urgentColor: "text-blue-400", - }, + const cards: { tab: Tab; label: string; counts: TabCounts | null; color: string; urgentColor: string }[] = [ + { tab: "reminders", label: "Напоминания", counts: null, color: "border-amber-500/20", urgentColor: "text-amber-400" }, + { tab: "classes", label: "Занятия", counts: counts.classes, color: "border-gold/20", urgentColor: "text-gold" }, + { tab: "master-classes", label: "Мастер-классы", counts: counts.mc, color: "border-purple-500/20", urgentColor: "text-purple-400" }, + { tab: "open-day", label: "Open Day", counts: counts.od, color: "border-cyan-500/20", urgentColor: "text-cyan-400" }, ]; - const hasWork = cards.some((c) => c.urgent > 0 || c.pending > 0); + const hasWork = cards.some((c) => { + if (c.counts) return c.counts.new + c.counts.contacted + c.counts.confirmed + c.counts.declined > 0; + return counts.remindersToday + counts.remindersTomorrow > 0; + }); if (!hasWork) return null; return (
{cards.map((c) => { - const total = c.urgent + c.pending; + // Reminders card + if (c.tab === "reminders") { + const total = counts.remindersToday + counts.remindersTomorrow; + if (total === 0) return ( +
+

{c.label}

+

+
+ ); + return ( + + ); + } + + // Booking cards — big numbers for new/contacted, small chips for confirmed/declined + const tc = c.counts!; + const total = tc.new + tc.contacted + tc.confirmed + tc.declined; if (total === 0) return (

{c.label}

@@ -671,24 +681,47 @@ function DashboardSummary({ onNavigate }: { onNavigate: (tab: Tab) => void }) {
); return ( -