From 49d710b2e79eae2d24783581b57d556900c731f1 Mon Sep 17 00:00:00 2001 From: "diana.dolgolyova" Date: Tue, 24 Mar 2026 17:15:47 +0300 Subject: [PATCH] refactor: move status filter from per-tab pills to global search bar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove FilterTabs from inside each booking tab - Add compact status chips (Все/Новая/Связались/Подтверждено/Отказ) below the search bar — one global filter for all tabs - Filter chips hidden during active text search - Status filter toggles on click (click again to deselect) - GenericBookingsList accepts filter as prop instead of managing internally --- .../admin/bookings/GenericBookingsList.tsx | 40 +++++------- src/app/admin/bookings/McRegistrationsTab.tsx | 5 +- src/app/admin/bookings/OpenDayBookingsTab.tsx | 5 +- src/app/admin/bookings/SearchBar.tsx | 62 ++++++++++++++----- src/app/admin/bookings/page.tsx | 14 +++-- 5 files changed, 77 insertions(+), 49 deletions(-) diff --git a/src/app/admin/bookings/GenericBookingsList.tsx b/src/app/admin/bookings/GenericBookingsList.tsx index fb281c4..0ec973f 100644 --- a/src/app/admin/bookings/GenericBookingsList.tsx +++ b/src/app/admin/bookings/GenericBookingsList.tsx @@ -3,8 +3,8 @@ import { useState, useMemo } from "react"; import { ChevronDown, ChevronRight, Archive } from "lucide-react"; import { adminFetch } from "@/lib/csrf"; -import { type BookingStatus, type BookingFilter, type BaseBooking, type BookingGroup, countStatuses, sortByStatus } from "./types"; -import { FilterTabs, EmptyState, BookingCard, ContactLinks, StatusBadge, StatusActions, DeleteBtn } from "./BookingComponents"; +import { type BookingStatus, type BookingFilter, type BaseBooking, type BookingGroup, sortByStatus } from "./types"; +import { EmptyState, BookingCard, ContactLinks, StatusBadge, StatusActions, DeleteBtn } from "./BookingComponents"; import { fmtDate } from "./types"; import { InlineNotes } from "./InlineNotes"; import { useToast } from "./Toast"; @@ -12,6 +12,7 @@ import { useToast } from "./Toast"; interface GenericBookingsListProps { items: T[]; endpoint: string; + filter: BookingFilter; onItemsChange: (fn: (prev: T[]) => T[]) => void; groups?: BookingGroup[]; renderExtra?: (item: T) => React.ReactNode; @@ -21,18 +22,16 @@ interface GenericBookingsListProps { export function GenericBookingsList({ items, endpoint, + filter, onItemsChange, groups, renderExtra, onConfirm, }: GenericBookingsListProps) { - const [filter, setFilter] = useState("all"); const [showArchived, setShowArchived] = useState(false); const [expanded, setExpanded] = useState>({}); const { showError } = useToast(); - const counts = useMemo(() => countStatuses(items), [items]); - async function handleStatus(id: number, status: BookingStatus) { if (status === "confirmed" && onConfirm) { onConfirm(id); @@ -113,18 +112,12 @@ export function GenericBookingsList({ const activeGroups = filteredGroups.filter((g) => !g.isArchived); const archivedGroups = filteredGroups.filter((g) => g.isArchived); const archivedCount = archivedGroups.reduce((sum, g) => sum + g.items.length, 0); - const allArchived = activeGroups.length === 0 && archivedCount > 0; - - // Count only active (non-archived) items for filter pills - const activeItems = items.filter((item) => { - const group = groups.find((g) => g.items.some((gi) => gi.id === item.id)); - return group && !group.isArchived; - }); - const activeCounts = countStatuses(activeItems); + const allArchived = activeGroups.length === 0 && archivedCount > 0 && filter === "all"; function renderGroup(group: BookingGroup) { const isOpen = expanded[group.key] ?? !group.isArchived; - const groupCounts = countStatuses(group.items); + const groupCounts = { new: 0, contacted: 0, confirmed: 0, declined: 0 }; + for (const item of group.items) groupCounts[item.status] = (groupCounts[item.status] || 0) + 1; 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 && ( + + )} +
+ {!isSearching && ( +
+ + + {BOOKING_STATUSES.map((s) => ( + + ))} +
)}
); diff --git a/src/app/admin/bookings/page.tsx b/src/app/admin/bookings/page.tsx index aed3058..a3aea30 100644 --- a/src/app/admin/bookings/page.tsx +++ b/src/app/admin/bookings/page.tsx @@ -4,7 +4,7 @@ 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 { adminFetch } from "@/lib/csrf"; -import { type BookingStatus, type SearchResult, BOOKING_STATUSES, SHORT_DAYS, fmtDate } from "./types"; +import { type BookingStatus, type BookingFilter, type SearchResult, BOOKING_STATUSES, SHORT_DAYS, fmtDate } from "./types"; import { LoadingSpinner, ContactLinks, BookingCard, StatusBadge, StatusActions, DeleteBtn } from "./BookingComponents"; import { GenericBookingsList } from "./GenericBookingsList"; import { AddBookingModal } from "./AddBookingModal"; @@ -230,7 +230,7 @@ function ConfirmModal({ interface ScheduleClassInfo { type: string; trainer: string; time: string; day: string; hall: string; address: string; groupId?: string } interface ScheduleLocation { name: string; address: string; days: { day: string; classes: { time: string; trainer: string; type: string; groupId?: string }[] }[] } -function GroupBookingsTab() { +function GroupBookingsTab({ filter }: { filter: BookingFilter }) { const [bookings, setBookings] = useState([]); const [allClasses, setAllClasses] = useState([]); const [loading, setLoading] = useState(true); @@ -294,6 +294,7 @@ function GroupBookingsTab() { items={bookings} endpoint="/api/admin/group-bookings" + filter={filter} onItemsChange={setBookings} onConfirm={(id) => setConfirmingId(id)} renderExtra={(b) => ( @@ -696,6 +697,7 @@ function BookingsPageInner() { const [tab, setTab] = useState("reminders"); const [addOpen, setAddOpen] = useState(false); const [searchResults, setSearchResults] = useState(null); + const [statusFilter, setStatusFilter] = useState("all"); const [newBookingsBanner, setNewBookingsBanner] = useState(false); const lastTotalRef = useRef(null); const { showError } = useToast(); @@ -782,6 +784,8 @@ function BookingsPageInner() { {/* Search */}
setSearchResults(null)} /> @@ -844,9 +848,9 @@ function BookingsPageInner() { {/* Tab content — no key={refreshKey}, banner handles new data */}
{tab === "reminders" && } - {tab === "classes" && } - {tab === "master-classes" && } - {tab === "open-day" && } + {tab === "classes" && } + {tab === "master-classes" && } + {tab === "open-day" && }
)}