refactor: move status filter from per-tab pills to global search bar
- 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
This commit is contained in:
@@ -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<T extends BaseBooking> {
|
||||
items: T[];
|
||||
endpoint: string;
|
||||
filter: BookingFilter;
|
||||
onItemsChange: (fn: (prev: T[]) => T[]) => void;
|
||||
groups?: BookingGroup<T>[];
|
||||
renderExtra?: (item: T) => React.ReactNode;
|
||||
@@ -21,18 +22,16 @@ interface GenericBookingsListProps<T extends BaseBooking> {
|
||||
export function GenericBookingsList<T extends BaseBooking>({
|
||||
items,
|
||||
endpoint,
|
||||
filter,
|
||||
onItemsChange,
|
||||
groups,
|
||||
renderExtra,
|
||||
onConfirm,
|
||||
}: GenericBookingsListProps<T>) {
|
||||
const [filter, setFilter] = useState<BookingFilter>("all");
|
||||
const [showArchived, setShowArchived] = useState(false);
|
||||
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
|
||||
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<T extends BaseBooking>({
|
||||
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<T>) {
|
||||
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 (
|
||||
<div key={group.key} className={`rounded-xl border overflow-hidden ${group.isArchived ? "border-white/5 opacity-60" : "border-white/10"}`}>
|
||||
<button
|
||||
@@ -166,16 +159,14 @@ export function GenericBookingsList<T extends BaseBooking>({
|
||||
|
||||
return (
|
||||
<div>
|
||||
{allArchived ? (
|
||||
{allArchived && (
|
||||
<p className="text-sm text-neutral-500 py-4">Все записи в архиве</p>
|
||||
) : (
|
||||
<>
|
||||
<FilterTabs filter={filter} counts={activeCounts} total={activeItems.length} onFilter={setFilter} />
|
||||
<div className="mt-3 space-y-2">
|
||||
{activeGroups.length === 0 && archivedGroups.length === 0 && <EmptyState total={items.length} />}
|
||||
{activeGroups.map(renderGroup)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!allArchived && (
|
||||
<div className="space-y-2">
|
||||
{activeGroups.length === 0 && archivedGroups.length === 0 && <EmptyState total={items.length} />}
|
||||
{activeGroups.map(renderGroup)}
|
||||
</div>
|
||||
)}
|
||||
{archivedCount > 0 && (
|
||||
<div className="mt-4">
|
||||
@@ -205,8 +196,7 @@ export function GenericBookingsList<T extends BaseBooking>({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FilterTabs filter={filter} counts={counts} total={items.length} onFilter={setFilter} />
|
||||
<div className="mt-3 space-y-2">
|
||||
<div className="space-y-2">
|
||||
{filtered.length === 0 && <EmptyState total={items.length} />}
|
||||
{filtered.map((item) => renderItem(item, false))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user